Rework Duplicate Dialog and Allow Migration (#492)

* (Mostly) Working Manga screen migration via duplicate dialog

* Fully working migrate from Browse Search

* Small tweaks for Antsy

* Update app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt

* Update app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit c0a888807b78891b28c6f6b9f16b719e24b03de1)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
This commit is contained in:
Maddie Witman 2024-03-22 09:04:43 -04:00 committed by Jobobby04
parent ec30ccccc2
commit 2ea488bff5
10 changed files with 230 additions and 60 deletions

View File

@ -1,16 +1,33 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.SwapVert
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -18,42 +35,92 @@ fun DuplicateMangaDialog(
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
modifier: Modifier = Modifier,
) {
AlertDialog(
val minHeight = LocalPreferenceMinHeight.current
AdaptiveSheet(
modifier = modifier,
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.are_you_sure))
},
text = {
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
Column(
modifier = Modifier
.padding(
vertical = TabbedDialogPaddings.Vertical,
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(TitlePadding),
text = stringResource(MR.strings.are_you_sure),
style = MaterialTheme.typography.headlineMedium,
)
Text(
text = stringResource(MR.strings.confirm_add_duplicate_manga),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(Modifier.height(PaddingSize))
TextPreferenceWidget(
title = stringResource(MR.strings.action_show_manga),
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_migrate_duplicate),
icon = Icons.Outlined.SwapVert,
onPreferenceClick = {
onDismissRequest()
onMigrate()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
)
Row(
modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
onDismissRequest()
onOpenManga()
},
) {
Text(text = stringResource(MR.strings.action_show_manga))
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
TextButton(
onClick = {
onDismissRequest()
onConfirm()
},
) {
Text(text = stringResource(MR.strings.action_add))
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(vertical = 8.dp),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleLarge,
fontSize = 16.sp,
)
}
}
},
)
}
}
}
private val PaddingSize = 16.dp
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)

View File

@ -46,9 +46,15 @@ import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import java.io.Serializable
import kotlin.math.roundToInt
class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
sealed class MigrationType : Serializable {
data class MangaList(val mangaIds: List<Long>) : MigrationType()
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
}
class PreMigrationScreen(val migration: MigrationType) : Screen() {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
@ -173,7 +179,7 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
screenModel.onMigrationSheet(false)
screenModel.saveEnabledSources()
navigator replace MigrationListScreen(MigrationProcedureConfig(mangaIds, extraParam))
navigator replace MigrationListScreen(MigrationProcedureConfig(migration, extraParam))
},
)
}
@ -184,10 +190,22 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(mangaIds, null),
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
)
} else {
PreMigrationScreen(mangaIds)
PreMigrationScreen(MigrationType.MangaList(mangaIds))
},
)
}
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaSingle(fromMangaId, toManga), null),
)
} else {
PreMigrationScreen(MigrationType.MangaSingle(fromMangaId, toManga))
},
)
}

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.util.system.toast
import exh.eh.EHentaiThrottleManager
@ -104,8 +105,14 @@ class MigrationListScreenModel(
init {
screenModelScope.launchIO {
val mangaIds = when (val migration = config.migration) {
is MigrationType.MangaList -> {
migration.mangaIds
}
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
}
runMigrations(
config.mangaIds
mangaIds
.map {
async {
val manga = getManga.await(it) ?: return@async null
@ -161,9 +168,13 @@ class MigrationListScreenModel(
break
}
// in case it was removed
if (manga.manga.id !in config.mangaIds) {
continue
when (val migration = config.migration) {
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
continue
}
else -> Unit
}
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
val mangaObj = manga.manga
val mangaSource = sourceManager.getOrStub(mangaObj.source)
@ -175,6 +186,28 @@ class MigrationListScreenModel(
} else {
sources.filter { it.id != mangaSource.id }
}
when (val migration = config.migration) {
is MigrationType.MangaSingle -> if (migration.toManga != null) {
val localManga = getManga.await(migration.toManga)
if (localManga != null) {
val source = sourceManager.get(localManga.source) as? CatalogueSource
if (source != null) {
val chapters = if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
} else {
source.getChapterList(localManga.toSManga())
}
try {
syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) {
}
manga.progress.value = validSources.size to validSources.size
return@async localManga
}
}
}
else -> Unit
}
if (useSourceWithMost) {
val sourceSemaphore = Semaphore(3)
val processedSources = AtomicInteger()
@ -523,13 +556,18 @@ class MigrationListScreenModel(
}
fun removeManga(item: MigratingManga) {
val ids = config.mangaIds.toMutableList()
val index = ids.indexOf(item.manga.id)
if (index > -1) {
ids.removeAt(index)
config.mangaIds = ids
val index2 = migratingItems.value.orEmpty().indexOf(item)
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
when (val migration = config.migration) {
is MigrationType.MangaList -> {
val ids = migration.mangaIds.toMutableList()
val index = ids.indexOf(item.manga.id)
if (index > -1) {
ids.removeAt(index)
config.migration = MigrationType.MangaList(ids)
val index2 = migratingItems.value.orEmpty().indexOf(item)
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
}
}
is MigrationType.MangaSingle -> Unit
}
}

View File

@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import java.io.Serializable
data class MigrationProcedureConfig(
var mangaIds: List<Long>,
var migration: MigrationType,
val extraSearchParams: String?,
) : Serializable

View File

@ -49,6 +49,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen
@ -62,6 +63,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
@ -69,6 +71,8 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.source.local.LocalSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class BrowseSourceScreen(
private val sourceId: Long,
@ -319,6 +323,16 @@ data class BrowseSourceScreen(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
dialog.duplicate.id,
dialog.manga.id,
)
// SY <--
},
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View File

@ -455,7 +455,7 @@ open class BrowseSourceScreenModel(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog
data class Migrate(val newManga: Manga) : Dialog
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
// SY -->
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog

View File

@ -251,11 +251,19 @@ class MangaScreen(
},
)
}
is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
)
is MangaScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = {
// SY -->
migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id)
// SY <--
},
)
}
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
onDismissRequest = onDismissRequest,
manga = successState.manga,
@ -457,12 +465,13 @@ class MangaScreen(
/**
* Initiates source migration for the specific manga.
*/
private fun migrateManga(navigator: Navigator, manga: Manga) {
private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
listOf(manga.id),
manga.id,
toMangaId,
)
// SY <--
}

View File

@ -1548,6 +1548,9 @@ class MangaScreenModel(
) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
/* SY -->
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
SY <-- */
data class SetFetchInterval(val manga: Manga) : Dialog
// SY -->
@ -1580,6 +1583,12 @@ class MangaScreenModel(
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
}
/* SY -->
fun showMigrateDialog(duplicate: Manga) {
val manga = successState?.manga ?: return
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
} SY <-- */
fun setExcludedScanlators(excludedScanlators: Set<String>) {
screenModelScope.launchIO {
setExcludedScanlators.await(mangaId, excludedScanlators)

View File

@ -20,15 +20,19 @@ import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import exh.ui.ifSourcesLoaded
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
@ -104,6 +108,14 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = {
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
dialog.duplicate.id,
dialog.manga.id,
)
}
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View File

@ -162,6 +162,8 @@
<string name="action_webview_refresh">Refresh</string>
<string name="action_start_downloading_now">Start downloading now</string>
<string name="action_not_now">Not now</string>
<string name="action_add_anyway">Add anyway</string>
<string name="action_migrate_duplicate">Migrate existing entry</string>
<!-- Operations -->
<string name="loading">Loading…</string>