diff --git a/app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt b/app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt new file mode 100644 index 000000000..64e1a5606 --- /dev/null +++ b/app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt @@ -0,0 +1,55 @@ +package eu.kanade.core.prefs + +import androidx.compose.ui.state.ToggleableState + +sealed class CheckboxState(open val value: T) { + abstract fun next(): CheckboxState + + sealed class State(override val value: T) : CheckboxState(value) { + data class Checked(override val value: T) : State(value) + data class None(override val value: T) : State(value) + + val isChecked: Boolean + get() = this is Checked + + override fun next(): CheckboxState { + return when (this) { + is Checked -> None(value) + is None -> Checked(value) + } + } + } + sealed class TriState(override val value: T) : CheckboxState(value) { + data class Include(override val value: T) : TriState(value) + data class Exclude(override val value: T) : TriState(value) + data class None(override val value: T) : TriState(value) + + override fun next(): CheckboxState { + 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.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State { + return if (condition(this)) { + CheckboxState.State.Checked(this) + } else { + CheckboxState.State.None(this) + } +} + +inline fun List.mapAsCheckboxState(condition: (T) -> Boolean): List> { + return this.map { it.asCheckboxState(condition) } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/ChangeCategoryDialog.kt b/app/src/main/java/eu/kanade/presentation/components/ChangeCategoryDialog.kt new file mode 100644 index 000000000..6d90168dd --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/ChangeCategoryDialog.kt @@ -0,0 +1,122 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import eu.kanade.core.prefs.CheckboxState +import eu.kanade.domain.category.model.Category +import eu.kanade.presentation.category.visualName +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.tachiyomi.R + +@Composable +fun ChangeCategoryDialog( + initialSelection: List>, + onDismissRequest: () -> Unit, + onEditCategories: () -> Unit, + onConfirm: (List, List) -> 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) -> 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), + ) + } + } + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/components/DeleteLibraryMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/components/DeleteLibraryMangaDialog.kt new file mode 100644 index 000000000..946d61935 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/DeleteLibraryMangaDialog.kt @@ -0,0 +1,78 @@ +package eu.kanade.tachiyomi.ui.library + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import eu.kanade.core.prefs.CheckboxState +import eu.kanade.tachiyomi.R + +@Composable +fun DeleteLibraryMangaDialog( + containsLocalManga: Boolean, + onDismissRequest: () -> Unit, + onConfirm: (Boolean, Boolean) -> Unit, +) { + var list by remember { + mutableStateOf( + buildList> { + 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) + list = mutableList.toList() + }, + ) + Text(text = stringResource(id = state.value)) + } + } + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt index 306509d64..7dddbebf2 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import eu.kanade.domain.category.model.Category import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryPresenter @Stable interface LibraryState { @@ -16,6 +17,7 @@ interface LibraryState { val selection: List val selectionMode: Boolean var hasActiveFilters: Boolean + var dialog: LibraryPresenter.Dialog? } fun LibraryState(): LibraryState { @@ -29,4 +31,5 @@ class LibraryStateImpl : LibraryState { override var selection: List by mutableStateOf(emptyList()) override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() } override var hasActiveFilters: Boolean by mutableStateOf(false) + override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/DownloadCustomChaptersDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/DownloadCustomChaptersDialog.kt new file mode 100644 index 000000000..dd1a236d7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/DownloadCustomChaptersDialog.kt @@ -0,0 +1,90 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChevronLeft +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material.icons.outlined.KeyboardDoubleArrowLeft +import androidx.compose.material.icons.outlined.KeyboardDoubleArrowRight +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import eu.kanade.tachiyomi.R + +@Composable +fun DownloadCustomAmountDialog( + maxAmount: Int, + onDismissRequest: () -> Unit, + onConfirm: (Int) -> Unit, +) { + var amount by remember { mutableStateOf(0) } + AlertDialog( + onDismissRequest = onDismissRequest, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onDismissRequest() + onConfirm(amount.coerceIn(0, maxAmount)) + }, + ) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + title = { + Text(text = stringResource(id = R.string.custom_download)) + }, + text = { + val onChangeAmount: (Int) -> Unit = { amount = (amount + it).coerceIn(0, maxAmount) } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { onChangeAmount(-10) }, + enabled = amount > 10, + ) { + Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "") + } + IconButton( + onClick = { onChangeAmount(-1) }, + enabled = amount > 0, + ) { + Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "") + } + BasicTextField( + value = amount.toString(), + onValueChange = { onChangeAmount(it.toIntOrNull() ?: 0) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + IconButton( + onClick = { onChangeAmount(1) }, + enabled = amount < maxAmount, + ) { + Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "") + } + IconButton( + onClick = { onChangeAmount(10) }, + enabled = amount < maxAmount, + ) { + Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "") + } + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt new file mode 100644 index 000000000..ef03eb47e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -0,0 +1,39 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R + +@Composable +fun DeleteChaptersDialog( + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onDismissRequest() + onConfirm() + }, + ) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + title = { + Text(text = stringResource(id = R.string.are_you_sure)) + }, + text = { + Text(text = stringResource(id = R.string.confirm_delete_chapters)) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt deleted file mode 100644 index 253add2da..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt +++ /dev/null @@ -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(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T : DeleteLibraryMangasDialog.Listener { - - private var mangas = emptyList() - - constructor(target: T, mangas: List) : 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, deleteFromLibrary: Boolean, deleteChapters: Boolean) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 2b0e1d154..ccc88f442 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -8,9 +8,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import eu.kanade.domain.category.model.Category +import eu.kanade.core.prefs.CheckboxState import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.presentation.components.ChangeCategoryDialog import eu.kanade.presentation.library.LibraryScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.toDomainManga @@ -19,20 +21,16 @@ import eu.kanade.tachiyomi.ui.base.controller.FullComposeController import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController +import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView import kotlinx.coroutines.cancel class LibraryController( bundle: Bundle? = null, -) : FullComposeController(bundle), - RootController, - ChangeMangaCategoriesDialog.Listener, - DeleteLibraryMangasDialog.Listener { +) : FullComposeController(bundle), RootController { /** * Sheet containing filter/sort/display items. @@ -65,6 +63,36 @@ class LibraryController( onClickSelectAll = { presenter.selectAll(presenter.activeCategory) }, onClickUnselectAll = ::clearSelection, ) + + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + is LibraryPresenter.Dialog.ChangeCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { + presenter.clearSelection() + router.pushController(CategoryController()) + }, + onConfirm = { include, exclude -> + presenter.clearSelection() + presenter.setMangaCategories(dialog.manga, include, exclude) + }, + ) + } + is LibraryPresenter.Dialog.DeleteManga -> { + DeleteLibraryMangaDialog( + containsLocalManga = dialog.manga.any(Manga::isLocal), + onDismissRequest = onDismissRequest, + onConfirm = { deleteManga, deleteChapter -> + presenter.removeMangas(dialog.manga.map { it.toDbManga() }, deleteManga, deleteChapter) + presenter.clearSelection() + }, + ) + } + null -> {} + } + LaunchedEffect(presenter.selectionMode) { val activity = (activity as? MainActivity) ?: return@LaunchedEffect // Could perhaps be removed when navigation is in a Compose world @@ -169,53 +197,40 @@ class LibraryController( private fun showMangaCategoriesDialog() { viewScope.launchIO { // Create a copy of selected manga - val mangas = presenter.selection.toList() + val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList() // Hide the default category because it has a different behavior than the ones from db. val categories = presenter.categories.filter { it.id != 0L } // Get indexes of the common categories to preselect. - val common = presenter.getCommonCategories(mangas.mapNotNull { it.toDomainManga() }) + val common = presenter.getCommonCategories(mangaList) // Get indexes of the mix categories to preselect. - val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() }) + val mix = presenter.getMixCategories(mangaList) val preselected = categories.map { when (it) { - in common -> QuadStateTextView.State.CHECKED.ordinal - in mix -> QuadStateTextView.State.INDETERMINATE.ordinal - else -> QuadStateTextView.State.UNCHECKED.ordinal + in common -> CheckboxState.State.Checked(it) + in mix -> CheckboxState.TriState.Exclude(it) + else -> CheckboxState.State.None(it) } - }.toTypedArray() - withUIContext { - ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected) - .showDialog(router) } + presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected) } } private fun downloadUnreadChapters() { - val mangas = presenter.selection.toList() - presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() }) + val mangaList = presenter.selection.toList() + presenter.downloadUnreadChapters(mangaList.mapNotNull { it.toDomainManga() }) presenter.clearSelection() } private fun markReadStatus(read: Boolean) { - val mangas = presenter.selection.toList() - presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read) + val mangaList = presenter.selection.toList() + presenter.markReadStatus(mangaList.mapNotNull { it.toDomainManga() }, read) presenter.clearSelection() } private fun showDeleteMangaDialog() { - val mangas = presenter.selection.toList() - DeleteLibraryMangasDialog(this, mangas.mapNotNull { it.toDomainManga() }).showDialog(router) - } - - override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { - presenter.setMangaCategories(mangas, addCategories, removeCategories) - presenter.clearSelection() - } - - override fun deleteMangas(mangas: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { - presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters) - presenter.clearSelection() + val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList() + presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 463d84cde..c88ae2394 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAny import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.PreferenceMutableState import eu.kanade.core.util.asFlow import eu.kanade.core.util.asObservable @@ -610,13 +611,15 @@ class LibraryPresenter( * @param addCategories the categories to add for all mangas. * @param removeCategories the categories to remove in all mangas. */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { presenterScope.launchIO { mangaList.map { manga -> val categoryIds = getCategories.await(manga.id) + .map { it.id } .subtract(removeCategories) .plus(addCategories) - .map { it.id } + .toList() + setMangaCategories.await(manga.id, categoryIds) } } @@ -715,4 +718,9 @@ class LibraryPresenter( val items = (loadedManga[category.id] ?: emptyList()).map { it.manga } state.selection = items.filterNot { it in selection } } + + sealed class Dialog { + data class ChangeCategory(val manga: List, val initialSelection: List>) : Dialog() + data class DeleteManga(val manga: List) : Dialog() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt index e3d4ec0bd..995deda1a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt @@ -2,10 +2,19 @@ package eu.kanade.tachiyomi.ui.manga import android.app.Dialog import android.os.Bundle +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import com.bluelinelabs.conductor.Controller import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.pushController @@ -46,3 +55,48 @@ class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) .create() } } + +@Composable +fun DuplicateDialog( + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, + onOpenManga: () -> Unit, + duplicateFrom: Source, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + Row { + TextButton(onClick = { + onDismissRequest() + onOpenManga() + },) { + Text(text = stringResource(id = R.string.action_show_manga)) + } + Spacer(modifier = Modifier.weight(1f)) + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = android.R.string.cancel)) + } + TextButton( + onClick = { + onDismissRequest() + onConfirm() + }, + ) { + Text(text = stringResource(id = R.string.action_add)) + } + } + }, + title = { + Text(text = stringResource(id = R.string.are_you_sure)) + }, + text = { + Text( + text = stringResource( + id = R.string.confirm_manga_add_duplicate, + duplicateFrom.name, + ), + ) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 4b62156e7..694867de1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -6,26 +6,26 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedDispatcherOwner -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.core.os.bundleOf import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.kanade.data.chapter.NoChaptersException -import eu.kanade.domain.category.model.Category -import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.presentation.components.ChangeCategoryDialog import eu.kanade.presentation.components.ChapterDownloadAction import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.MangaScreen +import eu.kanade.presentation.manga.components.DeleteChaptersDialog import eu.kanade.presentation.util.calculateWindowWidthSizeClass import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadService @@ -41,11 +41,12 @@ import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaPresenter.Dialog import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSettingsSheet -import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog +import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomAmountDialog import eu.kanade.tachiyomi.ui.manga.info.MangaFullCoverDialog import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog @@ -54,21 +55,13 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.recent.history.HistoryController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView -import eu.kanade.tachiyomi.widget.materialdialogs.await import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import logcat.LogPriority import eu.kanade.domain.chapter.model.Chapter as DomainChapter -class MangaController : - FullComposeController, - ChangeMangaCategoriesDialog.Listener, - DownloadCustomChaptersDialog.Listener { +class MangaController : FullComposeController { @Suppress("unused") constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) @@ -112,9 +105,19 @@ class MangaController : @Composable override fun ComposeContent() { val state by presenter.state.collectAsState() + val dialog by derivedStateOf { + when (val state = state) { + MangaScreenState.Loading -> null + is MangaScreenState.Success -> state.dialog + } + } + if (state is MangaScreenState.Success) { val successState = state as MangaScreenState.Success val isHttpSource = remember { successState.source is HttpSource } + + val scope = rememberCoroutineScope() + MangaScreen( state = successState, snackbarHostState = snackbarHostState, @@ -133,16 +136,67 @@ class MangaController : onCoverClicked = this::openCoverDialog, onShareClicked = this::shareManga.takeIf { isHttpSource }, onDownloadActionClicked = this::runDownloadChapterAction.takeIf { !successState.source.isLocalOrStub() }, - onEditCategoryClicked = this::onCategoriesClick.takeIf { successState.manga.favorite }, + onEditCategoryClicked = presenter::promptChangeCategories.takeIf { successState.manga.favorite }, onMigrateClicked = this::migrateManga.takeIf { successState.manga.favorite }, onMultiBookmarkClicked = presenter::bookmarkChapters, onMultiMarkAsReadClicked = presenter::markChaptersRead, onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead, - onMultiDeleteClicked = this::deleteChaptersWithConfirmation, + onMultiDeleteClicked = presenter::showDeleteChapterDialog, onChapterSelected = presenter::toggleSelection, onAllChapterSelected = presenter::toggleAllSelection, onInvertSelection = presenter::invertSelection, ) + + val onDismissRequest = { presenter.dismissDialog() } + when (val dialog = dialog) { + is Dialog.ChangeCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { + router.pushController(CategoryController()) + }, + onConfirm = { include, _ -> + presenter.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include) + }, + ) + } + is Dialog.DeleteChapters -> { + DeleteChaptersDialog( + onDismissRequest = onDismissRequest, + onConfirm = { + deleteChapters(dialog.chapters) + }, + ) + } + is Dialog.DownloadCustomAmount -> { + DownloadCustomAmountDialog( + maxAmount = dialog.max, + onDismissRequest = onDismissRequest, + onConfirm = { amount -> + val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount) + if (chaptersToDownload.isNotEmpty()) { + scope.launch { downloadChapters(chaptersToDownload) } + } + }, + ) + } + is Dialog.DuplicateManga -> { + DuplicateDialog( + onDismissRequest = onDismissRequest, + onConfirm = { + presenter.toggleFavorite( + onRemoved = {}, + onAdded = {}, + checkDuplicate = false, + ) + }, + onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) }, + duplicateFrom = presenter.getSourceOrStub(dialog.duplicate), + ) + } + null -> {} + } } else { LoadingScreen() } @@ -206,30 +260,10 @@ class MangaController : } } - private fun onFavoriteClick(checkDuplicate: Boolean = true) { + private fun onFavoriteClick() { presenter.toggleFavorite( onRemoved = this::onFavoriteRemoved, onAdded = { activity?.toast(activity?.getString(R.string.manga_added_library)) }, - onDuplicateExists = if (checkDuplicate) { - { - AddDuplicateMangaDialog( - target = this, - libraryManga = it, - onAddToLibrary = { onFavoriteClick(checkDuplicate = false) }, - ).showDialog(router) - } - } else null, - onRequireCategory = { manga, categories -> - val ids = runBlocking { presenter.getMangaCategoryIds(manga) } - val preselected = categories.map { - if (it.id in ids) { - QuadStateTextView.State.CHECKED.ordinal - } else { - QuadStateTextView.State.UNCHECKED.ordinal - } - }.toTypedArray() - showChangeCategoryDialog(manga, categories, preselected) - }, ) } @@ -249,40 +283,6 @@ class MangaController : } } - private fun onCategoriesClick() { - viewScope.launchIO { - val manga = presenter.manga ?: return@launchIO - val categories = presenter.getCategories() - - val ids = presenter.getMangaCategoryIds(manga) - val preselected = categories.map { - if (it.id in ids) { - QuadStateTextView.State.CHECKED.ordinal - } else { - QuadStateTextView.State.UNCHECKED.ordinal - } - }.toTypedArray() - - withUIContext { - showChangeCategoryDialog(manga, categories, preselected) - } - } - } - - private fun showChangeCategoryDialog(manga: Manga, categories: List, preselected: Array) { - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } - - override fun updateCategoriesForMangas( - mangas: List, - addCategories: List, - removeCategories: List, - ) { - val changed = mangas.firstOrNull() ?: return - presenter.moveMangaToCategoriesAndAddToLibrary(changed, addCategories) - } - /** * Perform a search using the provided query. * @@ -427,15 +427,6 @@ class MangaController : } } - private fun deleteChaptersWithConfirmation(chapters: List) { - 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) { if (chapters.isEmpty()) return presenter.deleteChapters(chapters) @@ -449,7 +440,7 @@ class MangaController : DownloadAction.NEXT_5_CHAPTERS -> presenter.getUnreadChaptersSorted().take(5) DownloadAction.NEXT_10_CHAPTERS -> presenter.getUnreadChaptersSorted().take(10) DownloadAction.CUSTOM -> { - showCustomDownloadDialog() + presenter.showDownloadCustomDialog() return } DownloadAction.UNREAD_CHAPTERS -> presenter.getUnreadChapters() @@ -462,21 +453,6 @@ class MangaController : } } - private fun showCustomDownloadDialog() { - val availableChapters = presenter.processedChapters?.count() ?: return - DownloadCustomChaptersDialog( - this, - availableChapters, - ).showDialog(router) - } - - override fun downloadCustomChapters(amount: Int) { - val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount) - if (chaptersToDownload.isNotEmpty()) { - viewScope.launch { downloadChapters(chaptersToDownload) } - } - } - // Chapters list - end // Tracker sheet - start diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 2e3ff998a..ab43f0bcb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -4,6 +4,8 @@ import android.app.Application import android.content.Context import android.os.Bundle import androidx.compose.runtime.Immutable +import eu.kanade.core.prefs.CheckboxState +import eu.kanade.core.prefs.mapAsCheckboxState import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.model.Category @@ -61,6 +63,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import logcat.LogPriority @@ -78,6 +81,7 @@ class MangaPresenter( val isFromSource: Boolean, private val preferences: PreferencesHelper = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), @@ -182,6 +186,7 @@ class MangaPresenter( isRefreshingChapter = true, isIncognitoMode = incognitoMode, isDownloadedOnlyMode = downloadedOnlyMode, + dialog = null, ) } @@ -259,8 +264,7 @@ class MangaPresenter( fun toggleFavorite( onRemoved: () -> Unit, onAdded: () -> Unit, - onRequireCategory: (manga: DomainManga, availableCats: List) -> Unit, - onDuplicateExists: ((DomainManga) -> Unit)?, + checkDuplicate: Boolean = true, ) { val state = successState ?: return presenterScope.launchIO { @@ -278,10 +282,16 @@ class MangaPresenter( } else { // Add to library // First, check if duplicate exists if callback is provided - if (onDuplicateExists != null) { + if (checkDuplicate) { val duplicate = getDuplicateLibraryManga.await(manga.title, manga.source) + if (duplicate != null) { - withUIContext { onDuplicateExists(duplicate) } + _state.update { state -> + when (state) { + MangaScreenState.Loading -> state + is MangaScreenState.Success -> state.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) + } + } return@launchIO } } @@ -308,7 +318,7 @@ class MangaPresenter( } // Choose a category - else -> withUIContext { onRequireCategory(manga, categories) } + else -> promptChangeCategories() } // Finally match with enhanced tracking when available @@ -334,6 +344,26 @@ class MangaPresenter( } } + fun promptChangeCategories() { + val state = successState ?: return + val manga = state.manga + presenterScope.launch { + val categories = getCategories() + val selection = getMangaCategoryIds(manga) + _state.update { state -> + when (state) { + MangaScreenState.Loading -> state + is MangaScreenState.Success -> state.copy( + dialog = Dialog.ChangeCategory( + manga = manga, + initialSelection = categories.mapAsCheckboxState { it.id in selection }, + ), + ) + } + } + } + } + /** * Returns true if the manga has any downloads. */ @@ -365,13 +395,13 @@ class MangaPresenter( * @param manga the manga to get categories from. * @return Array of category ids the manga is in, if none returns default id */ - suspend fun getMangaCategoryIds(manga: DomainManga): Array { - val categories = getCategories.await(manga.id) - return categories.map { it.id }.toTypedArray() + suspend fun getMangaCategoryIds(manga: DomainManga): List { + return getCategories.await(manga.id) + .map { it.id } } - fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { - moveMangaToCategories(categories) + fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { + moveMangaToCategory(categories) if (!manga.favorite) { presenterScope.launchIO { updateManga.awaitUpdateFavorite(manga.id, true) @@ -387,6 +417,10 @@ class MangaPresenter( */ private fun moveMangaToCategories(categories: List) { val categoryIds = categories.map { it.id } + moveMangaToCategory(categoryIds) + } + + fun moveMangaToCategory(categoryIds: List) { presenterScope.launchIO { setMangaCategories.await(mangaId, categoryIds) } @@ -994,6 +1028,45 @@ class MangaPresenter( } // Track sheet - end + + fun getSourceOrStub(manga: DomainManga): Source { + return sourceManager.getOrStub(manga.source) + } + + sealed class Dialog { + data class ChangeCategory(val manga: DomainManga, val initialSelection: List>) : Dialog() + data class DeleteChapters(val chapters: List) : 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) { + _state.update { state -> + when (state) { + MangaScreenState.Loading -> state + is MangaScreenState.Success -> state.copy(dialog = Dialog.DeleteChapters(chapters)) + } + } + } } sealed class MangaScreenState { @@ -1012,6 +1085,7 @@ sealed class MangaScreenState { val isRefreshingChapter: Boolean = false, val isIncognitoMode: Boolean = false, val isDownloadedOnlyMode: Boolean = false, + val dialog: MangaPresenter.Dialog? = null, ) : MangaScreenState() { val processedChapters: Sequence diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt deleted file mode 100644 index a1af94cc3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt +++ /dev/null @@ -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 : 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt deleted file mode 100644 index 7d90bd2c3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt +++ /dev/null @@ -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 -> "" - } - } -} diff --git a/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml deleted file mode 100644 index 128189cdf..000000000 --- a/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml deleted file mode 100644 index 1543ca119..000000000 --- a/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml deleted file mode 100644 index edc833548..000000000 --- a/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml deleted file mode 100644 index f73270046..000000000 --- a/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/download_custom_amount.xml b/app/src/main/res/layout/download_custom_amount.xml deleted file mode 100644 index e147cf4f4..000000000 --- a/app/src/main/res/layout/download_custom_amount.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f633ebf44..7814b6c1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -653,6 +653,7 @@ Also apply to all manga in my library Set as default No chapters found + Are you sure? AniList