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 package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.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.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -18,42 +35,92 @@ fun DuplicateMangaDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onOpenManga: () -> Unit, onOpenManga: () -> Unit,
onMigrate: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
AlertDialog( val minHeight = LocalPreferenceMinHeight.current
AdaptiveSheet(
modifier = modifier,
onDismissRequest = onDismissRequest, 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),
) { ) {
TextButton( Column(
onClick = { 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() onDismissRequest()
onOpenManga() onOpenManga()
}, },
) { )
Text(text = stringResource(MR.strings.action_show_manga))
}
Spacer(modifier = Modifier.weight(1f)) HorizontalDivider()
TextButton(onClick = onDismissRequest) { TextPreferenceWidget(
Text(text = stringResource(MR.strings.action_cancel)) title = stringResource(MR.strings.action_migrate_duplicate),
} icon = Icons.Outlined.SwapVert,
TextButton( onPreferenceClick = {
onClick = { onDismissRequest()
onMigrate()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest() onDismissRequest()
onConfirm() onConfirm()
}, },
) {
Text(text = stringResource(MR.strings.action_add))
}
}
},
) )
Row(
modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
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.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.io.Serializable
import kotlin.math.roundToInt 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 @Composable
override fun Content() { override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() } val screenModel = rememberScreenModel { PreMigrationScreenModel() }
@ -173,7 +179,7 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen() {
screenModel.onMigrationSheet(false) screenModel.onMigrationSheet(false)
screenModel.saveEnabledSources() 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( navigator.push(
if (skipPre) { if (skipPre) {
MigrationListScreen( MigrationListScreen(
MigrationProcedureConfig(mangaIds, null), MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
) )
} else { } 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.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags 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.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
@ -104,8 +105,14 @@ class MigrationListScreenModel(
init { init {
screenModelScope.launchIO { screenModelScope.launchIO {
val mangaIds = when (val migration = config.migration) {
is MigrationType.MangaList -> {
migration.mangaIds
}
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
}
runMigrations( runMigrations(
config.mangaIds mangaIds
.map { .map {
async { async {
val manga = getManga.await(it) ?: return@async null val manga = getManga.await(it) ?: return@async null
@ -161,9 +168,13 @@ class MigrationListScreenModel(
break break
} }
// in case it was removed // in case it was removed
if (manga.manga.id !in config.mangaIds) { when (val migration = config.migration) {
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
continue continue
} }
else -> Unit
}
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) { if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
val mangaObj = manga.manga val mangaObj = manga.manga
val mangaSource = sourceManager.getOrStub(mangaObj.source) val mangaSource = sourceManager.getOrStub(mangaObj.source)
@ -175,6 +186,28 @@ class MigrationListScreenModel(
} else { } else {
sources.filter { it.id != mangaSource.id } 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) { if (useSourceWithMost) {
val sourceSemaphore = Semaphore(3) val sourceSemaphore = Semaphore(3)
val processedSources = AtomicInteger() val processedSources = AtomicInteger()
@ -523,15 +556,20 @@ class MigrationListScreenModel(
} }
fun removeManga(item: MigratingManga) { fun removeManga(item: MigratingManga) {
val ids = config.mangaIds.toMutableList() when (val migration = config.migration) {
is MigrationType.MangaList -> {
val ids = migration.mangaIds.toMutableList()
val index = ids.indexOf(item.manga.id) val index = ids.indexOf(item.manga.id)
if (index > -1) { if (index > -1) {
ids.removeAt(index) ids.removeAt(index)
config.mangaIds = ids config.migration = MigrationType.MangaList(ids)
val index2 = migratingItems.value.orEmpty().indexOf(item) val index2 = migratingItems.value.orEmpty().indexOf(item)
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList() if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
} }
} }
is MigrationType.MangaSingle -> Unit
}
}
override fun onDispose() { override fun onDispose() {
super.onDispose() super.onDispose()

View File

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

View File

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

View File

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

View File

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

View File

@ -1548,6 +1548,9 @@ class MangaScreenModel(
) : Dialog ) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : 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 data class SetFetchInterval(val manga: Manga) : Dialog
// SY --> // SY -->
@ -1580,6 +1583,12 @@ class MangaScreenModel(
updateSuccessState { it.copy(dialog = Dialog.FullCover) } 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>) { fun setExcludedScanlators(excludedScanlators: Set<String>) {
screenModelScope.launchIO { screenModelScope.launchIO {
setExcludedScanlators.await(mangaId, excludedScanlators) 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.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen 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.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import exh.ui.ifSourcesLoaded import exh.ui.ifSourcesLoaded
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
@ -104,6 +108,14 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) }, onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, 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 -> { is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View File

@ -162,6 +162,8 @@
<string name="action_webview_refresh">Refresh</string> <string name="action_webview_refresh">Refresh</string>
<string name="action_start_downloading_now">Start downloading now</string> <string name="action_start_downloading_now">Start downloading now</string>
<string name="action_not_now">Not 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 --> <!-- Operations -->
<string name="loading">Loading…</string> <string name="loading">Loading…</string>