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)
This commit is contained in:
NarwhalHorns 2025-04-02 20:54:46 +01:00 committed by Cuong-Tran
parent d19c37596b
commit c2341cf3cd
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
13 changed files with 360 additions and 173 deletions

View file

@ -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)) - 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)) - 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 ### Fixes
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885)) - 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)) - 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))

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.manga 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding 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.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.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.Add
import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.SwapVert 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.AlertDialog
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.domain.ui.UiPreferences
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings 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.manga.components.RatioSwitchToPanorama
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget 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.manga.model.Manga
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@Composable @Composable
fun DuplicateMangaDialog( fun DuplicateMangaDialog(
duplicates: List<Manga>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onOpenManga: () -> Unit, onOpenManga: (manga: Manga) -> Unit,
onMigrate: () -> Unit, onMigrate: (manga: Manga) -> Unit,
// KMK -->
duplicate: Manga,
// KMK <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sourceManager = remember { Injekt.get<SourceManager>() }
val minHeight = LocalPreferenceMinHeight.current val minHeight = LocalPreferenceMinHeight.current
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
// KMK --> val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
val usePanoramaCover by Injekt.get<UiPreferences>().usePanoramaCoverAlways().collectAsState()
val coverRatio = remember { mutableFloatStateOf(1f) }
val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
// KMK <--
AdaptiveSheet( AdaptiveSheet(
modifier = modifier, modifier = modifier,
@ -73,115 +104,295 @@ fun DuplicateMangaDialog(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding( .padding(vertical = TabbedDialogPaddings.Vertical)
vertical = TabbedDialogPaddings.Vertical, .verticalScroll(rememberScrollState())
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(), .fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
Text( Text(
modifier = Modifier.padding(TitlePadding), text = stringResource(MR.strings.possible_duplicates_title),
text = stringResource(MR.strings.are_you_sure),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(top = MaterialTheme.padding.small),
) )
Text( Text(
text = stringResource(MR.strings.confirm_add_duplicate_manga), text = stringResource(MR.strings.possible_duplicates_summary),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
) )
Spacer(Modifier.height(PaddingSize)) LazyRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
// KMK --> modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
Box( contentPadding = horizontalPadding,
modifier = Modifier
.fillMaxWidth(),
) { ) {
if (usePanoramaCover && coverIsWide) { items(
MangaCover.Panorama( items = duplicates,
modifier = Modifier key = { it.id },
.align(Alignment.Center) ) {
.heightIn(max = 150.dp), DuplicateMangaListItem(
data = duplicate, manga = it,
onCoverLoaded = { _, result -> getSource = { sourceManager.getOrStub(it.source) },
val image = result.result.image onMigrate = { onMigrate(it) },
coverRatio.floatValue = image.height.toFloat() / image.width onDismissRequest = onDismissRequest,
}, onOpenManga = { onOpenManga(it) },
)
} 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
},
) )
} }
} }
// KMK <--
TextPreferenceWidget( Column(modifier = horizontalPaddingModifier) {
title = stringResource(MR.strings.action_show_manga), HorizontalDivider()
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
HorizontalDivider() TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
}
TextPreferenceWidget( OutlinedButton(
title = stringResource(MR.strings.action_migrate_duplicate), onClick = onDismissRequest,
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 modifier = Modifier
.sizeIn(minHeight = minHeight) .then(horizontalPaddingModifier)
.clickable { onDismissRequest.invoke() } .padding(bottom = MaterialTheme.padding.medium)
.padding(ButtonPadding) .heightIn(min = minHeight)
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) { ) {
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) { Text(
Text( modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
modifier = Modifier text = stringResource(MR.strings.action_cancel),
.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.primary,
text = stringResource(MR.strings.action_cancel), style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary, )
style = MaterialTheme.typography.titleLarge,
fontSize = 16.sp,
)
}
} }
} }
} }
} }
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) Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
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<Manga>): 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 --> // KMK -->
@Composable @Composable
@ -210,9 +421,9 @@ fun DuplicateMangasDialog(
}, },
text = { text = {
Column { 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( Box(
modifier = Modifier modifier = Modifier

View file

@ -195,7 +195,7 @@ class BulkFavoriteScreenModel(
private suspend fun getNotDuplicateLibraryMangas(): List<Manga> { private suspend fun getNotDuplicateLibraryMangas(): List<Manga> {
return state.value.selection.filterNot { manga -> return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty() getDuplicateLibraryManga(manga).isNotEmpty()
} }
} }
@ -203,7 +203,7 @@ class BulkFavoriteScreenModel(
val mangas = state.value.selection val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga -> mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga) val dup = getDuplicateLibraryManga(manga)
if (dup.isEmpty()) return@fastForEachIndexed if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first()) return Pair(index, dup.first())
} }
@ -294,10 +294,6 @@ class BulkFavoriteScreenModel(
.orEmpty() .orEmpty()
} }
private suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga).getOrNull(0)
}
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
} }
@ -374,16 +370,11 @@ class BulkFavoriteScreenModel(
fun addRemoveManga(manga: Manga, haptic: HapticFeedback? = null) { fun addRemoveManga(manga: Manga, haptic: HapticFeedback? = null) {
screenModelScope.launchIO { screenModelScope.launchIO {
val duplicateManga = getDuplicateLibraryManga(manga) val duplicates = getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> setDialog( manga.favorite -> setDialog(Dialog.RemoveManga(manga))
Dialog.RemoveManga(manga), duplicates.isNotEmpty() -> setDialog(
) Dialog.AddDuplicateManga(manga, duplicates),
duplicateManga != null -> setDialog(
Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
) )
else -> addFavorite(manga) else -> addFavorite(manga)
} }
@ -417,7 +408,7 @@ class BulkFavoriteScreenModel(
interface Dialog { interface Dialog {
data class RemoveManga(val manga: Manga) : 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<Manga>) : Dialog
data class ChangeMangaCategory( data class ChangeMangaCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>, val initialSelection: ImmutableList<CheckboxState.State<Category>>,
@ -445,18 +436,18 @@ fun AddDuplicateMangaDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) {
val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.AddDuplicateManga val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.AddDuplicateManga
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = bulkFavoriteScreenModel::dismissDialog, onDismissRequest = bulkFavoriteScreenModel::dismissDialog,
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) }, onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
}, },
duplicate = dialog.duplicate,
) )
} }

View file

@ -379,16 +379,11 @@ data class BrowseSourceScreen(
} else { } else {
// KMK <-- // KMK <--
scope.launchIO { scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga) val duplicates = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog( manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
BrowseSourceScreenModel.Dialog.RemoveManga(manga), duplicates.isNotEmpty() -> screenModel.setDialog(
) BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
) )
else -> screenModel.addFavorite(manga) else -> screenModel.addFavorite(manga)
} }
@ -450,22 +445,20 @@ data class BrowseSourceScreen(
} }
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
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(it.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
// SY <-- // SY <--
}, },
// KMK -->
duplicate = dialog.duplicate,
// KMK <--
) )
} }
is BrowseSourceScreenModel.Dialog.RemoveManga -> { is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View file

@ -434,8 +434,8 @@ open class BrowseSourceScreenModel(
.orEmpty() .orEmpty()
} }
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { suspend fun getDuplicateLibraryManga(manga: Manga): List<Manga> {
return getDuplicateLibraryManga.await(manga).getOrNull(0) return getDuplicateLibraryManga.invoke(manga)
} }
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
@ -488,7 +488,7 @@ open class BrowseSourceScreenModel(
sealed interface Dialog { sealed interface Dialog {
data object Filter : Dialog data object Filter : Dialog
data class RemoveManga(val manga: Manga) : 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<Manga>) : Dialog
data class ChangeMangaCategory( data class ChangeMangaCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>, val initialSelection: ImmutableList<CheckboxState.State<Category>>,

View file

@ -174,9 +174,9 @@ class HistoryScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO val manga = getManga.await(mangaId) ?: return@launchIO
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) val duplicates = getDuplicateLibraryManga(manga)
if (duplicate != null) { if (duplicates.isNotEmpty()) {
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
return@launchIO return@launchIO
} }
@ -240,7 +240,7 @@ class HistoryScreenModel(
sealed interface Dialog { sealed interface Dialog {
data object DeleteAll : Dialog data object DeleteAll : Dialog
data class Delete(val history: HistoryWithRelations) : 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<Manga>) : Dialog
data class ChangeCategory( data class ChangeCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,

View file

@ -115,24 +115,22 @@ data object HistoryTab : Tab {
} }
is HistoryScreenModel.Dialog.DuplicateManga -> { is HistoryScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { onConfirm = {
screenModel.addFavorite(dialog.manga) screenModel.addFavorite(dialog.manga)
}, },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
// KMK --> // KMK -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
// KMK <-- // KMK <--
}, },
// KMK -->
duplicate = dialog.duplicate,
// KMK <--
) )
} }
is HistoryScreenModel.Dialog.ChangeCategory -> { is HistoryScreenModel.Dialog.ChangeCategory -> {

View file

@ -464,17 +464,15 @@ class MangaScreen(
is MangaScreenModel.Dialog.DuplicateManga -> { is MangaScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
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(it.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id) migrateManga(navigator, it, screenModel.manga!!.id)
// SY <-- // SY <--
}, },
// KMK -->
duplicate = dialog.duplicate,
// KMK <--
) )
} }

View file

@ -785,10 +785,10 @@ class MangaScreenModel(
// Add to library // Add to library
// First, check if duplicate exists if callback is provided // First, check if duplicate exists if callback is provided
if (checkDuplicate) { if (checkDuplicate) {
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) val duplicates = getDuplicateLibraryManga(manga)
if (duplicate != null) { if (duplicates.isNotEmpty()) {
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
return@launchIO return@launchIO
} }
} }
@ -1737,7 +1737,7 @@ class MangaScreenModel(
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : 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 duplicates: List<Manga>) : Dialog
/* SY --> /* SY -->
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog

View file

@ -139,16 +139,11 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
} else { } else {
// KMK <-- // KMK <--
scope.launchIO { scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga) val duplicates = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog( manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
BrowseSourceScreenModel.Dialog.RemoveManga(manga), duplicates.isNotEmpty() -> screenModel.setDialog(
) BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
) )
else -> screenModel.addFavorite(manga) else -> screenModel.addFavorite(manga)
} }
@ -167,20 +162,18 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
is BrowseSourceScreenModel.Dialog.Migrate -> {} is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
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(it.id)) },
onMigrate = { onMigrate = {
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
}, },
// KMK -->
duplicate = dialog.duplicate,
// KMK <--
) )
} }
is BrowseSourceScreenModel.Dialog.RemoveManga -> { is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View file

@ -120,7 +120,7 @@ getDuplicateLibraryManga:
SELECT * SELECT *
FROM mangas FROM mangas
WHERE favorite = 1 WHERE favorite = 1
AND LOWER(title) = :title AND lower(title) LIKE '%' || lower(:title) || '%'
AND _id != :id; AND _id != :id;
getUpcomingManga: getUpcomingManga:

View file

@ -7,7 +7,7 @@ class GetDuplicateLibraryManga(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun await(manga: Manga): List<Manga> { suspend operator fun invoke(manga: Manga): List<Manga> {
return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase()) return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase())
} }
} }

View file

@ -166,7 +166,6 @@
<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_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>
@ -710,7 +709,8 @@
<!-- missing confirm menu after Compose rewrite #7901 --> <!-- missing confirm menu after Compose rewrite #7901 -->
<string name="remove_from_library">Remove from library</string> <string name="remove_from_library">Remove from library</string>
<string name="unknown_title">Unknown title</string> <string name="unknown_title">Unknown title</string>
<string name="confirm_add_duplicate_manga">You have an entry in your library with the same name.\n\nDo you still wish to continue?</string> <string name="possible_duplicates_title">Possible duplicates</string>
<string name="possible_duplicates_summary">You have entries in your library with a similar name.\n\nSelect an entry to migrate or add anyway.</string>
<string name="manga_added_library">Added to library</string> <string name="manga_added_library">Added to library</string>
<string name="manga_removed_library">Removed from library</string> <string name="manga_removed_library">Removed from library</string>
<string name="manga_info_expand">More</string> <string name="manga_info_expand">More</string>