Move a few Dialogs to Compose (#7861)

* Move a few Dialogs to Compose

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

* Review changes
This commit is contained in:
Andreas
2022-08-26 14:57:28 +02:00
committed by GitHub
parent 4b9a6541d1
commit 2453d1a886
20 changed files with 657 additions and 479 deletions
@@ -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"