mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01: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:
		
							
								
								
									
										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 eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryPresenter | ||||
|  | ||||
| @Stable | ||||
| interface LibraryState { | ||||
| @@ -16,6 +17,7 @@ interface LibraryState { | ||||
|     val selection: List<LibraryManga> | ||||
|     val selectionMode: Boolean | ||||
|     var hasActiveFilters: Boolean | ||||
|     var dialog: LibraryPresenter.Dialog? | ||||
| } | ||||
|  | ||||
| fun LibraryState(): LibraryState { | ||||
| @@ -29,4 +31,5 @@ class LibraryStateImpl : LibraryState { | ||||
|     override var selection: List<LibraryManga> by mutableStateOf(emptyList()) | ||||
|     override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() } | ||||
|     override var hasActiveFilters: Boolean by mutableStateOf(false) | ||||
|     override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null) | ||||
| } | ||||
|   | ||||
| @@ -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 com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.domain.manga.model.isLocal | ||||
| import eu.kanade.domain.manga.model.toDbManga | ||||
| import eu.kanade.presentation.components.ChangeCategoryDialog | ||||
| import eu.kanade.presentation.library.LibraryScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.toDomainManga | ||||
| @@ -19,20 +21,16 @@ import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView | ||||
| import kotlinx.coroutines.cancel | ||||
|  | ||||
| class LibraryController( | ||||
|     bundle: Bundle? = null, | ||||
| ) : FullComposeController<LibraryPresenter>(bundle), | ||||
|     RootController, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DeleteLibraryMangasDialog.Listener { | ||||
| ) : FullComposeController<LibraryPresenter>(bundle), RootController { | ||||
|  | ||||
|     /** | ||||
|      * Sheet containing filter/sort/display items. | ||||
| @@ -65,6 +63,36 @@ class LibraryController( | ||||
|             onClickSelectAll = { presenter.selectAll(presenter.activeCategory) }, | ||||
|             onClickUnselectAll = ::clearSelection, | ||||
|         ) | ||||
|  | ||||
|         val onDismissRequest = { presenter.dialog = null } | ||||
|         when (val dialog = presenter.dialog) { | ||||
|             is LibraryPresenter.Dialog.ChangeCategory -> { | ||||
|                 ChangeCategoryDialog( | ||||
|                     initialSelection = dialog.initialSelection, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onEditCategories = { | ||||
|                         presenter.clearSelection() | ||||
|                         router.pushController(CategoryController()) | ||||
|                     }, | ||||
|                     onConfirm = { include, exclude -> | ||||
|                         presenter.clearSelection() | ||||
|                         presenter.setMangaCategories(dialog.manga, include, exclude) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryPresenter.Dialog.DeleteManga -> { | ||||
|                 DeleteLibraryMangaDialog( | ||||
|                     containsLocalManga = dialog.manga.any(Manga::isLocal), | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onConfirm = { deleteManga, deleteChapter -> | ||||
|                         presenter.removeMangas(dialog.manga.map { it.toDbManga() }, deleteManga, deleteChapter) | ||||
|                         presenter.clearSelection() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             null -> {} | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(presenter.selectionMode) { | ||||
|             val activity = (activity as? MainActivity) ?: return@LaunchedEffect | ||||
|             // Could perhaps be removed when navigation is in a Compose world | ||||
| @@ -169,53 +197,40 @@ class LibraryController( | ||||
|     private fun showMangaCategoriesDialog() { | ||||
|         viewScope.launchIO { | ||||
|             // Create a copy of selected manga | ||||
|             val mangas = presenter.selection.toList() | ||||
|             val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList() | ||||
|  | ||||
|             // Hide the default category because it has a different behavior than the ones from db. | ||||
|             val categories = presenter.categories.filter { it.id != 0L } | ||||
|  | ||||
|             // Get indexes of the common categories to preselect. | ||||
|             val common = presenter.getCommonCategories(mangas.mapNotNull { it.toDomainManga() }) | ||||
|             val common = presenter.getCommonCategories(mangaList) | ||||
|             // Get indexes of the mix categories to preselect. | ||||
|             val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() }) | ||||
|             val mix = presenter.getMixCategories(mangaList) | ||||
|             val preselected = categories.map { | ||||
|                 when (it) { | ||||
|                     in common -> QuadStateTextView.State.CHECKED.ordinal | ||||
|                     in mix -> QuadStateTextView.State.INDETERMINATE.ordinal | ||||
|                     else -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|                     in common -> CheckboxState.State.Checked(it) | ||||
|                     in mix -> CheckboxState.TriState.Exclude(it) | ||||
|                     else -> CheckboxState.State.None(it) | ||||
|                 } | ||||
|             }.toTypedArray() | ||||
|             withUIContext { | ||||
|                 ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected) | ||||
|                     .showDialog(router) | ||||
|             } | ||||
|             presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun downloadUnreadChapters() { | ||||
|         val mangas = presenter.selection.toList() | ||||
|         presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() }) | ||||
|         val mangaList = presenter.selection.toList() | ||||
|         presenter.downloadUnreadChapters(mangaList.mapNotNull { it.toDomainManga() }) | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     private fun markReadStatus(read: Boolean) { | ||||
|         val mangas = presenter.selection.toList() | ||||
|         presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read) | ||||
|         val mangaList = presenter.selection.toList() | ||||
|         presenter.markReadStatus(mangaList.mapNotNull { it.toDomainManga() }, read) | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     private fun showDeleteMangaDialog() { | ||||
|         val mangas = presenter.selection.toList() | ||||
|         DeleteLibraryMangasDialog(this, mangas.mapNotNull { it.toDomainManga() }).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) { | ||||
|         presenter.setMangaCategories(mangas, addCategories, removeCategories) | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     override fun deleteMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) { | ||||
|         presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters) | ||||
|         presenter.clearSelection() | ||||
|         val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList() | ||||
|         presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.core.prefs.PreferenceMutableState | ||||
| import eu.kanade.core.util.asFlow | ||||
| import eu.kanade.core.util.asObservable | ||||
| @@ -610,13 +611,15 @@ class LibraryPresenter( | ||||
|      * @param addCategories the categories to add for all mangas. | ||||
|      * @param removeCategories the categories to remove in all mangas. | ||||
|      */ | ||||
|     fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) { | ||||
|     fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { | ||||
|         presenterScope.launchIO { | ||||
|             mangaList.map { manga -> | ||||
|                 val categoryIds = getCategories.await(manga.id) | ||||
|                     .map { it.id } | ||||
|                     .subtract(removeCategories) | ||||
|                     .plus(addCategories) | ||||
|                     .map { it.id } | ||||
|                     .toList() | ||||
|  | ||||
|                 setMangaCategories.await(manga.id, categoryIds) | ||||
|             } | ||||
|         } | ||||
| @@ -715,4 +718,9 @@ class LibraryPresenter( | ||||
|         val items = (loadedManga[category.id] ?: emptyList()).map { it.manga } | ||||
|         state.selection = items.filterNot { it in selection } | ||||
|     } | ||||
|  | ||||
|     sealed class Dialog { | ||||
|         data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog() | ||||
|         data class DeleteManga(val manga: List<Manga>) : Dialog() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|                 ), | ||||
|             ) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -6,26 +6,26 @@ import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.activity.OnBackPressedDispatcherOwner | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.material3.SnackbarResult | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.core.os.bundleOf | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.data.chapter.NoChaptersException | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.domain.manga.model.toDbManga | ||||
| import eu.kanade.presentation.components.ChangeCategoryDialog | ||||
| import eu.kanade.presentation.components.ChapterDownloadAction | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.presentation.manga.MangaScreen | ||||
| import eu.kanade.presentation.manga.components.DeleteChaptersDialog | ||||
| import eu.kanade.presentation.util.calculateWindowWidthSizeClass | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| @@ -41,11 +41,12 @@ import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController | ||||
| import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryController | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaPresenter.Dialog | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSettingsSheet | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog | ||||
| import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomAmountDialog | ||||
| import eu.kanade.tachiyomi.ui.manga.info.MangaFullCoverDialog | ||||
| import eu.kanade.tachiyomi.ui.manga.track.TrackItem | ||||
| import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog | ||||
| @@ -54,21 +55,13 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.ui.recent.history.HistoryController | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.await | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import logcat.LogPriority | ||||
| import eu.kanade.domain.chapter.model.Chapter as DomainChapter | ||||
|  | ||||
| class MangaController : | ||||
|     FullComposeController<MangaPresenter>, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DownloadCustomChaptersDialog.Listener { | ||||
| class MangaController : FullComposeController<MangaPresenter> { | ||||
|  | ||||
|     @Suppress("unused") | ||||
|     constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) | ||||
| @@ -112,9 +105,19 @@ class MangaController : | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         val state by presenter.state.collectAsState() | ||||
|         val dialog by derivedStateOf { | ||||
|             when (val state = state) { | ||||
|                 MangaScreenState.Loading -> null | ||||
|                 is MangaScreenState.Success -> state.dialog | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (state is MangaScreenState.Success) { | ||||
|             val successState = state as MangaScreenState.Success | ||||
|             val isHttpSource = remember { successState.source is HttpSource } | ||||
|  | ||||
|             val scope = rememberCoroutineScope() | ||||
|  | ||||
|             MangaScreen( | ||||
|                 state = successState, | ||||
|                 snackbarHostState = snackbarHostState, | ||||
| @@ -133,16 +136,67 @@ class MangaController : | ||||
|                 onCoverClicked = this::openCoverDialog, | ||||
|                 onShareClicked = this::shareManga.takeIf { isHttpSource }, | ||||
|                 onDownloadActionClicked = this::runDownloadChapterAction.takeIf { !successState.source.isLocalOrStub() }, | ||||
|                 onEditCategoryClicked = this::onCategoriesClick.takeIf { successState.manga.favorite }, | ||||
|                 onEditCategoryClicked = presenter::promptChangeCategories.takeIf { successState.manga.favorite }, | ||||
|                 onMigrateClicked = this::migrateManga.takeIf { successState.manga.favorite }, | ||||
|                 onMultiBookmarkClicked = presenter::bookmarkChapters, | ||||
|                 onMultiMarkAsReadClicked = presenter::markChaptersRead, | ||||
|                 onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead, | ||||
|                 onMultiDeleteClicked = this::deleteChaptersWithConfirmation, | ||||
|                 onMultiDeleteClicked = presenter::showDeleteChapterDialog, | ||||
|                 onChapterSelected = presenter::toggleSelection, | ||||
|                 onAllChapterSelected = presenter::toggleAllSelection, | ||||
|                 onInvertSelection = presenter::invertSelection, | ||||
|             ) | ||||
|  | ||||
|             val onDismissRequest = { presenter.dismissDialog() } | ||||
|             when (val dialog = dialog) { | ||||
|                 is Dialog.ChangeCategory -> { | ||||
|                     ChangeCategoryDialog( | ||||
|                         initialSelection = dialog.initialSelection, | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onEditCategories = { | ||||
|                             router.pushController(CategoryController()) | ||||
|                         }, | ||||
|                         onConfirm = { include, _ -> | ||||
|                             presenter.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include) | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|                 is Dialog.DeleteChapters -> { | ||||
|                     DeleteChaptersDialog( | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onConfirm = { | ||||
|                             deleteChapters(dialog.chapters) | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|                 is Dialog.DownloadCustomAmount -> { | ||||
|                     DownloadCustomAmountDialog( | ||||
|                         maxAmount = dialog.max, | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onConfirm = { amount -> | ||||
|                             val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount) | ||||
|                             if (chaptersToDownload.isNotEmpty()) { | ||||
|                                 scope.launch { downloadChapters(chaptersToDownload) } | ||||
|                             } | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|                 is Dialog.DuplicateManga -> { | ||||
|                     DuplicateDialog( | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onConfirm = { | ||||
|                             presenter.toggleFavorite( | ||||
|                                 onRemoved = {}, | ||||
|                                 onAdded = {}, | ||||
|                                 checkDuplicate = false, | ||||
|                             ) | ||||
|                         }, | ||||
|                         onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) }, | ||||
|                         duplicateFrom = presenter.getSourceOrStub(dialog.duplicate), | ||||
|                     ) | ||||
|                 } | ||||
|                 null -> {} | ||||
|             } | ||||
|         } else { | ||||
|             LoadingScreen() | ||||
|         } | ||||
| @@ -206,30 +260,10 @@ class MangaController : | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onFavoriteClick(checkDuplicate: Boolean = true) { | ||||
|     private fun onFavoriteClick() { | ||||
|         presenter.toggleFavorite( | ||||
|             onRemoved = this::onFavoriteRemoved, | ||||
|             onAdded = { activity?.toast(activity?.getString(R.string.manga_added_library)) }, | ||||
|             onDuplicateExists = if (checkDuplicate) { | ||||
|                 { | ||||
|                     AddDuplicateMangaDialog( | ||||
|                         target = this, | ||||
|                         libraryManga = it, | ||||
|                         onAddToLibrary = { onFavoriteClick(checkDuplicate = false) }, | ||||
|                     ).showDialog(router) | ||||
|                 } | ||||
|             } else null, | ||||
|             onRequireCategory = { manga, categories -> | ||||
|                 val ids = runBlocking { presenter.getMangaCategoryIds(manga) } | ||||
|                 val preselected = categories.map { | ||||
|                     if (it.id in ids) { | ||||
|                         QuadStateTextView.State.CHECKED.ordinal | ||||
|                     } else { | ||||
|                         QuadStateTextView.State.UNCHECKED.ordinal | ||||
|                     } | ||||
|                 }.toTypedArray() | ||||
|                 showChangeCategoryDialog(manga, categories, preselected) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -249,40 +283,6 @@ class MangaController : | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onCategoriesClick() { | ||||
|         viewScope.launchIO { | ||||
|             val manga = presenter.manga ?: return@launchIO | ||||
|             val categories = presenter.getCategories() | ||||
|  | ||||
|             val ids = presenter.getMangaCategoryIds(manga) | ||||
|             val preselected = categories.map { | ||||
|                 if (it.id in ids) { | ||||
|                     QuadStateTextView.State.CHECKED.ordinal | ||||
|                 } else { | ||||
|                     QuadStateTextView.State.UNCHECKED.ordinal | ||||
|                 } | ||||
|             }.toTypedArray() | ||||
|  | ||||
|             withUIContext { | ||||
|                 showChangeCategoryDialog(manga, categories, preselected) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: Array<Int>) { | ||||
|         ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) | ||||
|             .showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun updateCategoriesForMangas( | ||||
|         mangas: List<Manga>, | ||||
|         addCategories: List<Category>, | ||||
|         removeCategories: List<Category>, | ||||
|     ) { | ||||
|         val changed = mangas.firstOrNull() ?: return | ||||
|         presenter.moveMangaToCategoriesAndAddToLibrary(changed, addCategories) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Perform a search using the provided query. | ||||
|      * | ||||
| @@ -427,15 +427,6 @@ class MangaController : | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun deleteChaptersWithConfirmation(chapters: List<DomainChapter>) { | ||||
|         viewScope.launch { | ||||
|             val result = MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setMessage(R.string.confirm_delete_chapters) | ||||
|                 .await(android.R.string.ok, android.R.string.cancel) | ||||
|             if (result == AlertDialog.BUTTON_POSITIVE) deleteChapters(chapters) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun deleteChapters(chapters: List<DomainChapter>) { | ||||
|         if (chapters.isEmpty()) return | ||||
|         presenter.deleteChapters(chapters) | ||||
| @@ -449,7 +440,7 @@ class MangaController : | ||||
|             DownloadAction.NEXT_5_CHAPTERS -> presenter.getUnreadChaptersSorted().take(5) | ||||
|             DownloadAction.NEXT_10_CHAPTERS -> presenter.getUnreadChaptersSorted().take(10) | ||||
|             DownloadAction.CUSTOM -> { | ||||
|                 showCustomDownloadDialog() | ||||
|                 presenter.showDownloadCustomDialog() | ||||
|                 return | ||||
|             } | ||||
|             DownloadAction.UNREAD_CHAPTERS -> presenter.getUnreadChapters() | ||||
| @@ -462,21 +453,6 @@ class MangaController : | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun showCustomDownloadDialog() { | ||||
|         val availableChapters = presenter.processedChapters?.count() ?: return | ||||
|         DownloadCustomChaptersDialog( | ||||
|             this, | ||||
|             availableChapters, | ||||
|         ).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun downloadCustomChapters(amount: Int) { | ||||
|         val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount) | ||||
|         if (chaptersToDownload.isNotEmpty()) { | ||||
|             viewScope.launch { downloadChapters(chaptersToDownload) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Chapters list - end | ||||
|  | ||||
|     // Tracker sheet - start | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import android.app.Application | ||||
| import android.content.Context | ||||
| import android.os.Bundle | ||||
| import androidx.compose.runtime.Immutable | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.core.prefs.mapAsCheckboxState | ||||
| import eu.kanade.domain.category.interactor.GetCategories | ||||
| import eu.kanade.domain.category.interactor.SetMangaCategories | ||||
| import eu.kanade.domain.category.model.Category | ||||
| @@ -61,6 +63,7 @@ import kotlinx.coroutines.flow.filter | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.supervisorScope | ||||
| import kotlinx.coroutines.withContext | ||||
| import logcat.LogPriority | ||||
| @@ -78,6 +81,7 @@ class MangaPresenter( | ||||
|     val isFromSource: Boolean, | ||||
|     private val preferences: PreferencesHelper = Injekt.get(), | ||||
|     private val trackManager: TrackManager = Injekt.get(), | ||||
|     private val sourceManager: SourceManager = Injekt.get(), | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(), | ||||
|     private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), | ||||
| @@ -182,6 +186,7 @@ class MangaPresenter( | ||||
|                     isRefreshingChapter = true, | ||||
|                     isIncognitoMode = incognitoMode, | ||||
|                     isDownloadedOnlyMode = downloadedOnlyMode, | ||||
|                     dialog = null, | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
| @@ -259,8 +264,7 @@ class MangaPresenter( | ||||
|     fun toggleFavorite( | ||||
|         onRemoved: () -> Unit, | ||||
|         onAdded: () -> Unit, | ||||
|         onRequireCategory: (manga: DomainManga, availableCats: List<Category>) -> Unit, | ||||
|         onDuplicateExists: ((DomainManga) -> Unit)?, | ||||
|         checkDuplicate: Boolean = true, | ||||
|     ) { | ||||
|         val state = successState ?: return | ||||
|         presenterScope.launchIO { | ||||
| @@ -278,10 +282,16 @@ class MangaPresenter( | ||||
|             } else { | ||||
|                 // Add to library | ||||
|                 // First, check if duplicate exists if callback is provided | ||||
|                 if (onDuplicateExists != null) { | ||||
|                 if (checkDuplicate) { | ||||
|                     val duplicate = getDuplicateLibraryManga.await(manga.title, manga.source) | ||||
|  | ||||
|                     if (duplicate != null) { | ||||
|                         withUIContext { onDuplicateExists(duplicate) } | ||||
|                         _state.update { state -> | ||||
|                             when (state) { | ||||
|                                 MangaScreenState.Loading -> state | ||||
|                                 is MangaScreenState.Success -> state.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) | ||||
|                             } | ||||
|                         } | ||||
|                         return@launchIO | ||||
|                     } | ||||
|                 } | ||||
| @@ -308,7 +318,7 @@ class MangaPresenter( | ||||
|                     } | ||||
|  | ||||
|                     // Choose a category | ||||
|                     else -> withUIContext { onRequireCategory(manga, categories) } | ||||
|                     else -> promptChangeCategories() | ||||
|                 } | ||||
|  | ||||
|                 // Finally match with enhanced tracking when available | ||||
| @@ -334,6 +344,26 @@ class MangaPresenter( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun promptChangeCategories() { | ||||
|         val state = successState ?: return | ||||
|         val manga = state.manga | ||||
|         presenterScope.launch { | ||||
|             val categories = getCategories() | ||||
|             val selection = getMangaCategoryIds(manga) | ||||
|             _state.update { state -> | ||||
|                 when (state) { | ||||
|                     MangaScreenState.Loading -> state | ||||
|                     is MangaScreenState.Success -> state.copy( | ||||
|                         dialog = Dialog.ChangeCategory( | ||||
|                             manga = manga, | ||||
|                             initialSelection = categories.mapAsCheckboxState { it.id in selection }, | ||||
|                         ), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns true if the manga has any downloads. | ||||
|      */ | ||||
| @@ -365,13 +395,13 @@ class MangaPresenter( | ||||
|      * @param manga the manga to get categories from. | ||||
|      * @return Array of category ids the manga is in, if none returns default id | ||||
|      */ | ||||
|     suspend fun getMangaCategoryIds(manga: DomainManga): Array<Long> { | ||||
|         val categories = getCategories.await(manga.id) | ||||
|         return categories.map { it.id }.toTypedArray() | ||||
|     suspend fun getMangaCategoryIds(manga: DomainManga): List<Long> { | ||||
|         return getCategories.await(manga.id) | ||||
|             .map { it.id } | ||||
|     } | ||||
|  | ||||
|     fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List<Category>) { | ||||
|         moveMangaToCategories(categories) | ||||
|     fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List<Long>) { | ||||
|         moveMangaToCategory(categories) | ||||
|         if (!manga.favorite) { | ||||
|             presenterScope.launchIO { | ||||
|                 updateManga.awaitUpdateFavorite(manga.id, true) | ||||
| @@ -387,6 +417,10 @@ class MangaPresenter( | ||||
|      */ | ||||
|     private fun moveMangaToCategories(categories: List<Category>) { | ||||
|         val categoryIds = categories.map { it.id } | ||||
|         moveMangaToCategory(categoryIds) | ||||
|     } | ||||
|  | ||||
|     fun moveMangaToCategory(categoryIds: List<Long>) { | ||||
|         presenterScope.launchIO { | ||||
|             setMangaCategories.await(mangaId, categoryIds) | ||||
|         } | ||||
| @@ -994,6 +1028,45 @@ class MangaPresenter( | ||||
|     } | ||||
|  | ||||
|     // Track sheet - end | ||||
|  | ||||
|     fun getSourceOrStub(manga: DomainManga): Source { | ||||
|         return sourceManager.getOrStub(manga.source) | ||||
|     } | ||||
|  | ||||
|     sealed class Dialog { | ||||
|         data class ChangeCategory(val manga: DomainManga, val initialSelection: List<CheckboxState<Category>>) : Dialog() | ||||
|         data class DeleteChapters(val chapters: List<DomainChapter>) : Dialog() | ||||
|         data class DuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog() | ||||
|         data class DownloadCustomAmount(val max: Int) : Dialog() | ||||
|     } | ||||
|  | ||||
|     fun dismissDialog() { | ||||
|         _state.update { state -> | ||||
|             when (state) { | ||||
|                 MangaScreenState.Loading -> state | ||||
|                 is MangaScreenState.Success -> state.copy(dialog = null) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun showDownloadCustomDialog() { | ||||
|         val max = processedChapters?.count() ?: return | ||||
|         _state.update { state -> | ||||
|             when (state) { | ||||
|                 MangaScreenState.Loading -> state | ||||
|                 is MangaScreenState.Success -> state.copy(dialog = Dialog.DownloadCustomAmount(max)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun showDeleteChapterDialog(chapters: List<DomainChapter>) { | ||||
|         _state.update { state -> | ||||
|             when (state) { | ||||
|                 MangaScreenState.Loading -> state | ||||
|                 is MangaScreenState.Success -> state.copy(dialog = Dialog.DeleteChapters(chapters)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class MangaScreenState { | ||||
| @@ -1012,6 +1085,7 @@ sealed class MangaScreenState { | ||||
|         val isRefreshingChapter: Boolean = false, | ||||
|         val isIncognitoMode: Boolean = false, | ||||
|         val isDownloadedOnlyMode: Boolean = false, | ||||
|         val dialog: MangaPresenter.Dialog? = null, | ||||
|     ) : MangaScreenState() { | ||||
|  | ||||
|         val processedChapters: Sequence<ChapterItem> | ||||
|   | ||||
| @@ -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="set_chapter_settings_as_default">Set as default</string> | ||||
|     <string name="no_chapters_error">No chapters found</string> | ||||
|     <string name="are_you_sure">Are you sure?</string> | ||||
|  | ||||
|     <!-- Tracking Screen --> | ||||
|     <string name="tracker_anilist" translatable="false">AniList</string> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user