Display total chapters on duplicates list items (#1963)

(cherry picked from commit 12abd9938b7c235d6a1c02391624703476c1f339)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
#	data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
This commit is contained in:
NarwhalHorns 2025-04-07 18:33:49 +01:00 committed by Jobobby04
parent f1aed0d8b9
commit 5e0f730159
10 changed files with 152 additions and 29 deletions

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.manga
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable 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.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -63,10 +64,14 @@ import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -74,7 +79,7 @@ import uy.kohesive.injekt.api.get
@Composable @Composable
fun DuplicateMangaDialog( fun DuplicateMangaDialog(
duplicates: List<Manga>, duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onOpenManga: (manga: Manga) -> Unit, onOpenManga: (manga: Manga) -> Unit,
@ -118,14 +123,14 @@ fun DuplicateMangaDialog(
) { ) {
items( items(
items = duplicates, items = duplicates,
key = { it.id }, key = { it.manga.id },
) { ) {
DuplicateMangaListItem( DuplicateMangaListItem(
manga = it, duplicate = it,
getSource = { sourceManager.getOrStub(it.source) }, getSource = { sourceManager.getOrStub(it.manga.source) },
onMigrate = { onMigrate(it) }, onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it) }, onOpenManga = { onOpenManga(it.manga) },
) )
} }
} }
@ -165,13 +170,14 @@ fun DuplicateMangaDialog(
@Composable @Composable
private fun DuplicateMangaListItem( private fun DuplicateMangaListItem(
manga: Manga, duplicate: MangaWithChapterCount,
getSource: () -> Source, getSource: () -> Source,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onOpenManga: () -> Unit, onOpenManga: () -> Unit,
onMigrate: () -> Unit, onMigrate: () -> Unit,
) { ) {
val source = getSource() val source = getSource()
val manga = duplicate.manga
Column( Column(
modifier = Modifier modifier = Modifier
.width(MangaCardWidth) .width(MangaCardWidth)
@ -186,6 +192,7 @@ private fun DuplicateMangaListItem(
) )
.padding(MaterialTheme.padding.small), .padding(MaterialTheme.padding.small),
) { ) {
Box {
MangaCover.Book( MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current) data = ImageRequest.Builder(LocalContext.current)
.data(manga) .data(manga)
@ -193,6 +200,22 @@ private fun DuplicateMangaListItem(
.build(), .build(),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
Badge(
color = MaterialTheme.colorScheme.secondary,
textColor = MaterialTheme.colorScheme.onSecondary,
text = pluralStringResource(
MR.plurals.manga_num_chapters,
duplicate.chapterCount.toInt(),
duplicate.chapterCount,
),
)
}
}
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall)) Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
@ -292,7 +315,7 @@ private fun MangaDetailRow(
} }
@Composable @Composable
private fun getMaximumMangaCardHeight(duplicates: List<Manga>): Dp { private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
val density = LocalDensity.current val density = LocalDensity.current
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
@ -320,7 +343,7 @@ private fun getMaximumMangaCardHeight(duplicates: List<Manga>): Dp {
) { ) {
duplicates.fastMaxOfOrNull { duplicates.fastMaxOfOrNull {
calculateMangaCardHeight( calculateMangaCardHeight(
manga = it, manga = it.manga,
density = density, density = density,
typography = typography, typography = typography,
textMeasurer = textMeasurer, textMeasurer = textMeasurer,

View File

@ -65,6 +65,7 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFlatMetadataById import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.interactor.DeleteSavedSearchById import tachiyomi.domain.source.interactor.DeleteSavedSearchById
import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.interactor.GetRemoteManga
@ -393,7 +394,7 @@ open class BrowseSourceScreenModel(
.orEmpty() .orEmpty()
} }
suspend fun getDuplicateLibraryManga(manga: Manga): List<Manga> { suspend fun getDuplicateLibraryManga(manga: Manga): List<MangaWithChapterCount> {
return getDuplicateLibraryManga.invoke(manga) return getDuplicateLibraryManga.invoke(manga)
} }
@ -444,7 +445,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 duplicates: List<Manga>) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : 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

@ -40,6 +40,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -246,7 +247,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 duplicates: List<Manga>) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : 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

@ -125,6 +125,7 @@ import tachiyomi.domain.manga.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.model.CustomMangaInfo import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
import tachiyomi.domain.manga.model.MergedMangaReference import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.manga.model.applyFilter
@ -1653,7 +1654,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 duplicates: List<Manga>) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : 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

@ -3,6 +3,7 @@ package tachiyomi.data.manga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.view.LibraryView import tachiyomi.view.LibraryView
object MangaMapper { object MangaMapper {
@ -143,6 +144,71 @@ object MangaMapper {
lastRead = lastRead, lastRead = lastRead,
) )
fun mapMangaWithChapterCount(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
// SY -->
@Suppress("UNUSED_PARAMETER")
filteredScanlators: String?,
// SY <--
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
version: Long,
isSyncing: Long,
notes: String,
totalCount: Long,
): MangaWithChapterCount = MangaWithChapterCount(
manga = mapManga(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
// SY -->
null,
// SY <--
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
version,
isSyncing,
notes,
),
chapterCount = totalCount,
)
fun mapLibraryView(libraryView: LibraryView): LibraryManga { fun mapLibraryView(libraryView: LibraryView): LibraryManga {
return LibraryManga( return LibraryManga(
manga = Manga( manga = Manga(

View File

@ -11,6 +11,7 @@ import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
@ -73,9 +74,9 @@ class MangaRepositoryImpl(
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) } return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
} }
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga> { override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
return handler.awaitList { return handler.awaitList {
mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga) mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
} }
} }

View File

@ -117,11 +117,33 @@ WHERE favorite = 1
AND source = :sourceId; AND source = :sourceId;
getDuplicateLibraryManga: getDuplicateLibraryManga:
WITH
duplicates AS (
SELECT * SELECT *
FROM mangas FROM mangas
WHERE favorite = 1 WHERE favorite = 1
AND _id != :id
AND lower(title) LIKE '%' || lower(:title) || '%' AND lower(title) LIKE '%' || lower(:title) || '%'
AND _id != :id; ),
chapter_counts AS (
SELECT
M._id AS manga_id,
count(*) AS chapter_count
FROM duplicates M
JOIN chapters C
ON M._id = C.manga_id
LEFT JOIN excluded_scanlators ES
ON C.manga_id = ES.manga_id
AND C.scanlator = ES.scanlator
WHERE ES.scanlator IS NULL
GROUP BY M._id
)
SELECT
M.*,
coalesce(CC.chapter_count, 0) AS chapter_count
FROM duplicates M
LEFT JOIN chapter_counts CC
ON M._id = CC.manga_id;
getUpcomingManga: getUpcomingManga:
SELECT * SELECT *

View File

@ -1,13 +1,14 @@
package tachiyomi.domain.manga.interactor package tachiyomi.domain.manga.interactor
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
class GetDuplicateLibraryManga( class GetDuplicateLibraryManga(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend operator fun invoke(manga: Manga): List<Manga> { suspend operator fun invoke(manga: Manga): List<MangaWithChapterCount> {
return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase()) return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase())
} }
} }

View File

@ -0,0 +1,6 @@
package tachiyomi.domain.manga.model
data class MangaWithChapterCount(
val manga: Manga,
val chapterCount: Long,
)

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
interface MangaRepository { interface MangaRepository {
@ -25,7 +26,7 @@ interface MangaRepository {
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga> suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount>
suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>>