Display all similarly named duplicates in duplicate manga dialogue (#1861)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 0d35b6fdafbf5451a2743ea9bcfc735bf49374a7) # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
This commit is contained in:
parent
b0f645d906
commit
615adc567b
@ -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.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@ -8,37 +9,82 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.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.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.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.Typography
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
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.source.model.StubSource
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
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
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
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,
|
||||||
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)
|
||||||
|
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@ -46,81 +92,292 @@ 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),
|
||||||
TextPreferenceWidget(
|
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
|
||||||
title = stringResource(MR.strings.action_show_manga),
|
contentPadding = horizontalPadding,
|
||||||
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,
|
|
||||||
) {
|
) {
|
||||||
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
items(
|
||||||
Text(
|
items = duplicates,
|
||||||
modifier = Modifier
|
key = { it.id },
|
||||||
.padding(vertical = 8.dp),
|
) {
|
||||||
text = stringResource(MR.strings.action_cancel),
|
DuplicateMangaListItem(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
manga = it,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
getSource = { sourceManager.getOrStub(it.source) },
|
||||||
fontSize = 16.sp,
|
onMigrate = { onMigrate(it) },
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onOpenManga = { onOpenManga(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(modifier = horizontalPaddingModifier) {
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.action_add_anyway),
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
onPreferenceClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onConfirm()
|
||||||
|
},
|
||||||
|
modifier = Modifier.clip(CircleShape),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onDismissRequest,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(horizontalPaddingModifier)
|
||||||
|
.padding(bottom = MaterialTheme.padding.medium)
|
||||||
|
.heightIn(min = minHeight)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
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)
|
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
|
||||||
|
@ -256,14 +256,11 @@ data class BrowseSourceScreen(
|
|||||||
onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) },
|
onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) },
|
||||||
onMangaLongClick = { manga ->
|
onMangaLongClick = { manga ->
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
val duplicates = screenModel.getDuplicateLibraryManga(manga)
|
||||||
when {
|
when {
|
||||||
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
||||||
duplicateManga != null -> screenModel.setDialog(
|
duplicates.isNotEmpty() -> screenModel.setDialog(
|
||||||
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
|
||||||
manga,
|
|
||||||
duplicateManga,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else -> screenModel.addFavorite(manga)
|
else -> screenModel.addFavorite(manga)
|
||||||
}
|
}
|
||||||
@ -318,15 +315,16 @@ 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 <--
|
||||||
|
@ -393,8 +393,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) {
|
||||||
@ -444,7 +444,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>>,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +246,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>>,
|
||||||
|
@ -114,17 +114,18 @@ 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 = {
|
||||||
// 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 <--
|
||||||
|
@ -261,12 +261,13 @@ 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 <--
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -783,10 +783,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1653,7 +1653,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
|
||||||
|
@ -103,14 +103,15 @@ 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,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -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:
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
@ -708,7 +707,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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user