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.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.model.SManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
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.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
@ -74,7 +79,7 @@ import uy.kohesive.injekt.api.get
@Composable
fun DuplicateMangaDialog(
duplicates: List<Manga>,
duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
onOpenManga: (manga: Manga) -> Unit,
@ -118,14 +123,14 @@ fun DuplicateMangaDialog(
) {
items(
items = duplicates,
key = { it.id },
key = { it.manga.id },
) {
DuplicateMangaListItem(
manga = it,
getSource = { sourceManager.getOrStub(it.source) },
onMigrate = { onMigrate(it) },
duplicate = it,
getSource = { sourceManager.getOrStub(it.manga.source) },
onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it) },
onOpenManga = { onOpenManga(it.manga) },
)
}
}
@ -165,13 +170,14 @@ fun DuplicateMangaDialog(
@Composable
private fun DuplicateMangaListItem(
manga: Manga,
duplicate: MangaWithChapterCount,
getSource: () -> Source,
onDismissRequest: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
) {
val source = getSource()
val manga = duplicate.manga
Column(
modifier = Modifier
.width(MangaCardWidth)
@ -186,13 +192,30 @@ private fun DuplicateMangaListItem(
)
.padding(MaterialTheme.padding.small),
) {
MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
modifier = Modifier.fillMaxWidth(),
)
Box {
MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
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))
@ -292,7 +315,7 @@ private fun MangaDetailRow(
}
@Composable
private fun getMaximumMangaCardHeight(duplicates: List<Manga>): Dp {
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
val density = LocalDensity.current
val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer()
@ -320,7 +343,7 @@ private fun getMaximumMangaCardHeight(duplicates: List<Manga>): Dp {
) {
duplicates.fastMaxOfOrNull {
calculateMangaCardHeight(
manga = it,
manga = it.manga,
density = density,
typography = typography,
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.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.interactor.DeleteSavedSearchById
import tachiyomi.domain.source.interactor.GetRemoteManga
@ -393,7 +394,7 @@ open class BrowseSourceScreenModel(
.orEmpty()
}
suspend fun getDuplicateLibraryManga(manga: Manga): List<Manga> {
suspend fun getDuplicateLibraryManga(manga: Manga): List<MangaWithChapterCount> {
return getDuplicateLibraryManga.invoke(manga)
}
@ -444,7 +445,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 duplicates: List<Manga>) : Dialog
data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
data class ChangeMangaCategory(
val manga: Manga,
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.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -246,7 +247,7 @@ class HistoryScreenModel(
sealed interface Dialog {
data object DeleteAll : 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(
val manga: Manga,
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.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.manga.model.applyFilter
@ -1653,7 +1654,7 @@ class MangaScreenModel(
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : 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 -->
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 tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.view.LibraryView
object MangaMapper {
@ -143,6 +144,71 @@ object MangaMapper {
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 {
return LibraryManga(
manga = Manga(

View File

@ -11,6 +11,7 @@ import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.repository.MangaRepository
import java.time.LocalDate
import java.time.ZoneId
@ -73,9 +74,9 @@ class MangaRepositoryImpl(
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 {
mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga)
mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
}
}

View File

@ -117,11 +117,33 @@ WHERE favorite = 1
AND source = :sourceId;
getDuplicateLibraryManga:
SELECT *
FROM mangas
WHERE favorite = 1
AND lower(title) LIKE '%' || lower(:title) || '%'
AND _id != :id;
WITH
duplicates AS (
SELECT *
FROM mangas
WHERE favorite = 1
AND _id != :id
AND lower(title) LIKE '%' || lower(:title) || '%'
),
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:
SELECT *

View File

@ -1,13 +1,14 @@
package tachiyomi.domain.manga.interactor
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.repository.MangaRepository
class GetDuplicateLibraryManga(
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())
}
}

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.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
interface MangaRepository {
@ -25,7 +26,7 @@ interface MangaRepository {
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>>