From c2341cf3cddf2919ca279fe2f977a50c52cd25ce Mon Sep 17 00:00:00 2001 From: NarwhalHorns Date: Wed, 2 Apr 2025 20:54:46 +0100 Subject: [PATCH] Display all similarly named duplicates in duplicate manga dialogue (mihonapp/mihon#1861) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 0d35b6fdafbf5451a2743ea9bcfc735bf49374a7) --- CHANGELOG.md | 3 + .../manga/DuplicateMangaDialog.kt | 413 +++++++++++++----- .../ui/browse/BulkFavoriteScreenModel.kt | 29 +- .../source/browse/BrowseSourceScreen.kt | 21 +- .../source/browse/BrowseSourceScreenModel.kt | 6 +- .../ui/history/HistoryScreenModel.kt | 8 +- .../kanade/tachiyomi/ui/history/HistoryTab.kt | 8 +- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 8 +- .../tachiyomi/ui/manga/MangaScreenModel.kt | 8 +- .../exh/md/follows/MangaDexFollowsScreen.kt | 21 +- .../main/sqldelight/tachiyomi/data/mangas.sq | 2 +- .../interactor/GetDuplicateLibraryManga.kt | 2 +- .../moko-resources/base/strings.xml | 4 +- 13 files changed, 360 insertions(+), 173 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c471aa16..e78a377ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946)) - Deduplicate entries when browsing ([@AntsyLich](https://github.com/AntsyLich)) ([#1957](https://github.com/mihonapp/mihon/pull/1957)) +### Changed +- Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861)) + ### Fixes - Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885)) - Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920)) diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index d20617299..d681abc94 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.manga -import androidx.compose.foundation.clickable +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,26 +14,55 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Brush +import androidx.compose.material.icons.filled.PersonOutline +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Book -import androidx.compose.material.icons.outlined.SwapVert +import androidx.compose.material.icons.outlined.AttachMoney +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.DoneAll +import androidx.compose.material.icons.outlined.Pause +import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material3.AlertDialog import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastMaxOfOrNull +import coil3.request.ImageRequest +import coil3.request.crossfade import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.TabbedDialogPaddings @@ -40,32 +70,33 @@ import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.RatioSwitchToPanorama import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.model.StubSource +import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState +import tachiyomi.presentation.core.util.secondaryItemAlpha import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @Composable fun DuplicateMangaDialog( + duplicates: List, onDismissRequest: () -> Unit, onConfirm: () -> Unit, - onOpenManga: () -> Unit, - onMigrate: () -> Unit, - // KMK --> - duplicate: Manga, - // KMK <-- + onOpenManga: (manga: Manga) -> Unit, + onMigrate: (manga: Manga) -> Unit, modifier: Modifier = Modifier, ) { + val sourceManager = remember { Injekt.get() } val minHeight = LocalPreferenceMinHeight.current - - // KMK --> - val usePanoramaCover by Injekt.get().usePanoramaCoverAlways().collectAsState() - val coverRatio = remember { mutableFloatStateOf(1f) } - val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama - // KMK <-- + val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal) + val horizontalPaddingModifier = Modifier.padding(horizontalPadding) AdaptiveSheet( modifier = modifier, @@ -73,115 +104,295 @@ fun DuplicateMangaDialog( ) { Column( modifier = Modifier - .padding( - vertical = TabbedDialogPaddings.Vertical, - horizontal = TabbedDialogPaddings.Horizontal, - ) + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()) .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { Text( - modifier = Modifier.padding(TitlePadding), - text = stringResource(MR.strings.are_you_sure), + text = stringResource(MR.strings.possible_duplicates_title), style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .then(horizontalPaddingModifier) + .padding(top = MaterialTheme.padding.small), ) Text( - text = stringResource(MR.strings.confirm_add_duplicate_manga), + text = stringResource(MR.strings.possible_duplicates_summary), style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.then(horizontalPaddingModifier), ) - Spacer(Modifier.height(PaddingSize)) - - // KMK --> - Box( - modifier = Modifier - .fillMaxWidth(), + LazyRow( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)), + contentPadding = horizontalPadding, ) { - if (usePanoramaCover && coverIsWide) { - MangaCover.Panorama( - modifier = Modifier - .align(Alignment.Center) - .heightIn(max = 150.dp), - data = duplicate, - onCoverLoaded = { _, result -> - val image = result.result.image - coverRatio.floatValue = image.height.toFloat() / image.width - }, - ) - } else { - MangaCover.Book( - modifier = Modifier - .align(Alignment.Center) - .widthIn(max = 150.dp), - data = duplicate, - onCoverLoaded = { _, result -> - val image = result.result.image - coverRatio.floatValue = image.height.toFloat() / image.width - }, + items( + items = duplicates, + key = { it.id }, + ) { + DuplicateMangaListItem( + manga = it, + getSource = { sourceManager.getOrStub(it.source) }, + onMigrate = { onMigrate(it) }, + onDismissRequest = onDismissRequest, + onOpenManga = { onOpenManga(it) }, ) } } - // KMK <-- - TextPreferenceWidget( - title = stringResource(MR.strings.action_show_manga), - icon = Icons.Outlined.Book, - onPreferenceClick = { - onDismissRequest() - onOpenManga() - }, - ) + Column(modifier = horizontalPaddingModifier) { + HorizontalDivider() - HorizontalDivider() + TextPreferenceWidget( + title = stringResource(MR.strings.action_add_anyway), + icon = Icons.Outlined.Add, + onPreferenceClick = { + onDismissRequest() + onConfirm() + }, + modifier = Modifier.clip(CircleShape), + ) + } - 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( + OutlinedButton( + onClick = onDismissRequest, modifier = Modifier - .sizeIn(minHeight = minHeight) - .clickable { onDismissRequest.invoke() } - .padding(ButtonPadding) + .then(horizontalPaddingModifier) + .padding(bottom = MaterialTheme.padding.medium) + .heightIn(min = minHeight) .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, - ) - } + Text( + modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall), + text = stringResource(MR.strings.action_cancel), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodyLarge, + ) } } } } -private val PaddingSize = 16.dp +@Composable +private fun DuplicateMangaListItem( + manga: Manga, + getSource: () -> Source, + onDismissRequest: () -> Unit, + onOpenManga: () -> Unit, + onMigrate: () -> Unit, +) { + val source = getSource() + Column( + modifier = Modifier + .width(MangaCardWidth) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surface) + .combinedClickable( + onLongClick = { onOpenManga() }, + onClick = { + onDismissRequest() + onMigrate() + }, + ) + .padding(MaterialTheme.padding.small), + ) { + MangaCover.Book( + data = ImageRequest.Builder(LocalContext.current) + .data(manga) + .crossfade(true) + .build(), + modifier = Modifier.fillMaxWidth(), + ) -private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp) -private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp) + Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall)) + + Text( + text = manga.title, + style = MaterialTheme.typography.titleSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + + if (!manga.author.isNullOrBlank()) { + MangaDetailRow( + text = manga.author!!, + iconImageVector = Icons.Filled.PersonOutline, + maxLines = 2, + ) + } + + if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) { + MangaDetailRow( + text = manga.artist!!, + iconImageVector = Icons.Filled.Brush, + maxLines = 2, + ) + } + + MangaDetailRow( + text = when (manga.status) { + SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing) + SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed) + SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed) + SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished) + SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled) + SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus) + else -> stringResource(MR.strings.unknown) + }, + iconImageVector = when (manga.status) { + SManga.ONGOING.toLong() -> Icons.Outlined.Schedule + SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll + SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney + SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done + SManga.CANCELLED.toLong() -> Icons.Outlined.Close + SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause + else -> Icons.Outlined.Block + }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + if (source is StubSource) { + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.error, + ) + } + Text( + text = source.name, + style = MaterialTheme.typography.labelSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } +} + +@Composable +private fun MangaDetailRow( + text: String, + iconImageVector: ImageVector, + maxLines: Int = 1, +) { + Row( + modifier = Modifier + .secondaryItemAlpha() + .padding(top = MaterialTheme.padding.extraSmall), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = iconImageVector, + contentDescription = null, + modifier = Modifier.size(MangaDetailsIconWidth), + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + maxLines = maxLines, + ) + } +} + +@Composable +private fun getMaximumMangaCardHeight(duplicates: List): Dp { + val density = LocalDensity.current + val typography = MaterialTheme.typography + val textMeasurer = rememberTextMeasurer() + + val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() } + val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() } + + val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) } + val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() } + + val coverHeight = width / MangaCover.Book.ratio + val constraints = Constraints(maxWidth = width) + val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding) + + return remember( + duplicates, + density, + typography, + textMeasurer, + smallPadding, + extraSmallPadding, + coverHeight, + constraints, + detailsConstraints, + ) { + duplicates.fastMaxOfOrNull { + calculateMangaCardHeight( + manga = it, + density = density, + typography = typography, + textMeasurer = textMeasurer, + smallPadding = smallPadding, + extraSmallPadding = extraSmallPadding, + coverHeight = coverHeight, + constraints = constraints, + detailsConstraints = detailsConstraints, + ) + } + ?: 0.dp + } +} + +private fun calculateMangaCardHeight( + manga: Manga, + density: Density, + typography: Typography, + textMeasurer: TextMeasurer, + smallPadding: Int, + extraSmallPadding: Int, + coverHeight: Float, + constraints: Constraints, + detailsConstraints: Constraints, +): Dp { + val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints) + val authorHeight = if (!manga.author.isNullOrBlank()) { + textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints) + } else { + 0 + } + val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) { + textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints) + } else { + 0 + } + val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints) + val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints) + + val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight + return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() } +} + +private fun TextMeasurer.measureHeight( + text: String, + style: TextStyle, + maxLines: Int, + constraints: Constraints, +): Int = measure( + text = text, + style = style, + overflow = TextOverflow.Ellipsis, + maxLines = maxLines, + constraints = constraints, +) + .size + .height + +private val MangaCardWidth = 150.dp +private val MangaDetailsIconWidth = 16.dp // KMK --> @Composable @@ -210,9 +421,9 @@ fun DuplicateMangasDialog( }, text = { Column { - Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) + Text(text = stringResource(MR.strings.possible_duplicates_title)) - Spacer(Modifier.height(PaddingSize)) + Spacer(Modifier.height(MaterialTheme.padding.medium)) Box( modifier = Modifier diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 8106b48fa..1828e55ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -195,7 +195,7 @@ class BulkFavoriteScreenModel( private suspend fun getNotDuplicateLibraryMangas(): List { return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() + getDuplicateLibraryManga(manga).isNotEmpty() } } @@ -203,7 +203,7 @@ class BulkFavoriteScreenModel( val mangas = state.value.selection mangas.fastForEachIndexed { index, manga -> if (index < startIdx) return@fastForEachIndexed - val dup = getDuplicateLibraryManga.await(manga) + val dup = getDuplicateLibraryManga(manga) if (dup.isEmpty()) return@fastForEachIndexed return Pair(index, dup.first()) } @@ -294,10 +294,6 @@ class BulkFavoriteScreenModel( .orEmpty() } - private suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { - return getDuplicateLibraryManga.await(manga).getOrNull(0) - } - private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) } @@ -374,16 +370,11 @@ class BulkFavoriteScreenModel( fun addRemoveManga(manga: Manga, haptic: HapticFeedback? = null) { screenModelScope.launchIO { - val duplicateManga = getDuplicateLibraryManga(manga) + val duplicates = getDuplicateLibraryManga(manga) when { - manga.favorite -> setDialog( - Dialog.RemoveManga(manga), - ) - duplicateManga != null -> setDialog( - Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), + manga.favorite -> setDialog(Dialog.RemoveManga(manga)) + duplicates.isNotEmpty() -> setDialog( + Dialog.AddDuplicateManga(manga, duplicates), ) else -> addFavorite(manga) } @@ -417,7 +408,7 @@ class BulkFavoriteScreenModel( interface Dialog { data class RemoveManga(val manga: Manga) : Dialog - data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class AddDuplicateManga(val manga: Manga, val duplicates: List) : Dialog data class ChangeMangaCategory( val manga: Manga, val initialSelection: ImmutableList>, @@ -445,18 +436,18 @@ fun AddDuplicateMangaDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.AddDuplicateManga DuplicateMangaDialog( + duplicates = dialog.duplicates, onDismissRequest = bulkFavoriteScreenModel::dismissDialog, onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) }, - onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), navigator, - dialog.duplicate.id, + it.id, dialog.manga.id, ) }, - duplicate = dialog.duplicate, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 4929db0c1..39457c5d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -379,16 +379,11 @@ data class BrowseSourceScreen( } else { // KMK <-- scope.launchIO { - val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + val duplicates = screenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.RemoveManga(manga), - ) - duplicateManga != null -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), + manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) + duplicates.isNotEmpty() -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates), ) else -> screenModel.addFavorite(manga) } @@ -450,22 +445,20 @@ data class BrowseSourceScreen( } is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { DuplicateMangaDialog( + duplicates = dialog.duplicates, onDismissRequest = onDismissRequest, onConfirm = { screenModel.addFavorite(dialog.manga) }, - onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { // SY --> PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), navigator, - dialog.duplicate.id, + it.id, dialog.manga.id, ) // SY <-- }, - // KMK --> - duplicate = dialog.duplicate, - // KMK <-- ) } is BrowseSourceScreenModel.Dialog.RemoveManga -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index f51288666..79e983a24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -434,8 +434,8 @@ open class BrowseSourceScreenModel( .orEmpty() } - suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { - return getDuplicateLibraryManga.await(manga).getOrNull(0) + suspend fun getDuplicateLibraryManga(manga: Manga): List { + return getDuplicateLibraryManga.invoke(manga) } private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { @@ -488,7 +488,7 @@ open class BrowseSourceScreenModel( sealed interface Dialog { data object Filter : Dialog data class RemoveManga(val manga: Manga) : Dialog - data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class AddDuplicateManga(val manga: Manga, val duplicates: List) : Dialog data class ChangeMangaCategory( val manga: Manga, val initialSelection: ImmutableList>, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 28c98f926..cdc72585f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -174,9 +174,9 @@ class HistoryScreenModel( screenModelScope.launchIO { val manga = getManga.await(mangaId) ?: return@launchIO - val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) - if (duplicate != null) { - mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } + val duplicates = getDuplicateLibraryManga(manga) + if (duplicates.isNotEmpty()) { + mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } return@launchIO } @@ -240,7 +240,7 @@ class HistoryScreenModel( sealed interface Dialog { data object DeleteAll : Dialog data class Delete(val history: HistoryWithRelations) : Dialog - data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class DuplicateManga(val manga: Manga, val duplicates: List) : Dialog data class ChangeCategory( val manga: Manga, val initialSelection: ImmutableList>, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 2a269f1da..0d309d667 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -115,24 +115,22 @@ data object HistoryTab : Tab { } is HistoryScreenModel.Dialog.DuplicateManga -> { DuplicateMangaDialog( + duplicates = dialog.duplicates, onDismissRequest = onDismissRequest, onConfirm = { screenModel.addFavorite(dialog.manga) }, - onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { // KMK --> PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), navigator, - dialog.duplicate.id, + it.id, dialog.manga.id, ) // KMK <-- }, - // KMK --> - duplicate = dialog.duplicate, - // KMK <-- ) } is HistoryScreenModel.Dialog.ChangeCategory -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 4a642c666..11b595407 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -464,17 +464,15 @@ class MangaScreen( is MangaScreenModel.Dialog.DuplicateManga -> { DuplicateMangaDialog( + duplicates = dialog.duplicates, onDismissRequest = onDismissRequest, onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) }, - onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { // SY --> - migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id) + migrateManga(navigator, it, screenModel.manga!!.id) // SY <-- }, - // KMK --> - duplicate = dialog.duplicate, - // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 43b63f76a..88b551206 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -785,10 +785,10 @@ class MangaScreenModel( // Add to library // First, check if duplicate exists if callback is provided if (checkDuplicate) { - val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) + val duplicates = getDuplicateLibraryManga(manga) - if (duplicate != null) { - updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } + if (duplicates.isNotEmpty()) { + updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } return@launchIO } } @@ -1737,7 +1737,7 @@ class MangaScreenModel( val initialSelection: ImmutableList>, ) : Dialog data class DeleteChapters(val chapters: List) : Dialog - data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class DuplicateManga(val manga: Manga, val duplicates: List) : Dialog /* SY --> data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 9fb4ef413..3349000ca 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -139,16 +139,11 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { } else { // KMK <-- scope.launchIO { - val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + val duplicates = screenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.RemoveManga(manga), - ) - duplicateManga != null -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), + manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) + duplicates.isNotEmpty() -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates), ) else -> screenModel.addFavorite(manga) } @@ -167,20 +162,18 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { is BrowseSourceScreenModel.Dialog.Migrate -> {} is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { DuplicateMangaDialog( + duplicates = dialog.duplicates, onDismissRequest = onDismissRequest, onConfirm = { screenModel.addFavorite(dialog.manga) }, - onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), navigator, - dialog.duplicate.id, + it.id, dialog.manga.id, ) }, - // KMK --> - duplicate = dialog.duplicate, - // KMK <-- ) } is BrowseSourceScreenModel.Dialog.RemoveManga -> { diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index dd2a4a3c9..cf0de16ce 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -120,7 +120,7 @@ getDuplicateLibraryManga: SELECT * FROM mangas WHERE favorite = 1 -AND LOWER(title) = :title +AND lower(title) LIKE '%' || lower(:title) || '%' AND _id != :id; getUpcomingManga: diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt index df5cec44a..b81d73b6c 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetDuplicateLibraryManga.kt @@ -7,7 +7,7 @@ class GetDuplicateLibraryManga( private val mangaRepository: MangaRepository, ) { - suspend fun await(manga: Manga): List { + suspend operator fun invoke(manga: Manga): List { return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase()) } } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 86d9b9962..aee73d03a 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -166,7 +166,6 @@ Start downloading now Not now Add anyway - Migrate existing entry Loading… @@ -710,7 +709,8 @@ Remove from library Unknown title - You have an entry in your library with the same name.\n\nDo you still wish to continue? + Possible duplicates + You have entries in your library with a similar name.\n\nSelect an entry to migrate or add anyway. Added to library Removed from library More