Multiple authors support

This commit is contained in:
Koitharu
2025-03-24 14:12:15 +02:00
parent 24cf2a2725
commit 424c4d8827
9 changed files with 83 additions and 46 deletions

View File

@@ -87,6 +87,10 @@ fun <T, R> Collection<T>.mapSortedByCount(isDescending: Boolean = true, mapper:
return sorted.map { it.first }
}
fun Collection<CharSequence?>.contains(element: CharSequence?, ignoreCase: Boolean): Boolean = any { x ->
(x == null && element == null) || (x != null && element != null && x.contains(element, ignoreCase))
}
fun Collection<CharSequence?>.indexOfContains(element: CharSequence?, ignoreCase: Boolean): Int = indexOfFirst { x ->
(x == null && element == null) || (x != null && element != null && x.contains(element, ignoreCase))
}

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.details.ui
import android.text.Spannable
import android.text.TextPaint
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
class AuthorSpan(private val listener: OnAuthorClickListener) : ClickableSpan() {
override fun onClick(widget: View) {
val text = (widget as? TextView)?.text as? Spannable ?: return
val start = text.getSpanStart(this)
val end = text.getSpanEnd(this)
val selected = text.substring(start, end).trim()
if (selected.isNotEmpty()) {
listener.onAuthorClick(selected)
}
}
override fun updateDrawState(ds: TextPaint) {
ds.setColor(ds.linkColor)
}
fun interface OnAuthorClickListener {
fun onAuthorClick(author: String)
}
}

View File

@@ -2,12 +2,15 @@ package org.koitharu.kotatsu.details.ui
import android.content.Context
import android.os.Bundle
import android.text.SpannedString
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
@@ -118,7 +121,7 @@ class DetailsActivity :
View.OnClickListener,
View.OnLayoutChangeListener, ViewTreeObserver.OnDrawListener,
ChipsView.OnChipClickListener, OnListItemClickListener<Bookmark>,
SwipeRefreshLayout.OnRefreshListener {
SwipeRefreshLayout.OnRefreshListener, AuthorSpan.OnAuthorClickListener {
@Inject
lateinit var shortcutManager: AppShortcutManager
@@ -138,7 +141,6 @@ class DetailsActivity :
supportActionBar?.setDisplayShowTitleEnabled(false)
viewBinding.chipFavorite.setOnClickListener(this)
infoBinding.textViewLocal.setOnClickListener(this)
infoBinding.textViewAuthor.setOnClickListener(this)
infoBinding.textViewSource.setOnClickListener(this)
viewBinding.imageViewCover.setOnClickListener(this)
viewBinding.textViewTitle.setOnClickListener(this)
@@ -148,6 +150,7 @@ class DetailsActivity :
viewBinding.textViewDescription.addOnLayoutChangeListener(this)
viewBinding.swipeRefreshLayout.setOnRefreshListener(this)
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
infoBinding.textViewAuthor.movementMethod = LinkMovementMethodCompat.getInstance()
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
viewBinding.chipsTags.onChipClickListener = this
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
@@ -199,29 +202,23 @@ class DetailsActivity :
override fun onClick(v: View) {
when (v.id) {
R.id.textView_author -> {
val manga = viewModel.manga.value
val author = manga?.author ?: return
router.showAuthorDialog(author, manga.source)
}
R.id.textView_source -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.openList(manga.source, null, null)
}
R.id.textView_local -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.showLocalInfoDialog(manga)
}
R.id.chip_favorite -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.showFavoriteDialog(manga)
}
R.id.imageView_cover -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.openImage(
url = viewModel.coverUrl.value ?: return,
source = manga.source,
@@ -245,17 +242,17 @@ class DetailsActivity :
}
R.id.button_scrobbling_more -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.showScrobblingSelectorSheet(manga, null)
}
R.id.button_related_more -> {
val manga = viewModel.manga.value ?: return
val manga = viewModel.getMangaOrNull() ?: return
router.openRelated(manga)
}
R.id.textView_title -> {
val title = viewModel.manga.value?.title?.nullIfEmpty() ?: return
val title = viewModel.getMangaOrNull()?.title?.nullIfEmpty() ?: return
buildAlertDialog(this) {
setMessage(title)
setNegativeButton(R.string.close, null)
@@ -267,6 +264,10 @@ class DetailsActivity :
}
}
override fun onAuthorClick(author: String) {
router.showAuthorDialog(author, viewModel.getMangaOrNull()?.source ?: return)
}
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
router.showTagDialog(tag)
@@ -415,7 +416,7 @@ class DetailsActivity :
TextDrawable.compound(infoBinding.textViewTranslation, it)
}
infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible
textViewAuthor.textAndVisible = manga.author
textViewAuthor.textAndVisible = manga.getAuthorsString()
textViewAuthorLabel.isVisible = textViewAuthor.isVisible
if (manga.hasRating) {
ratingBarRating.rating = manga.rating * ratingBarRating.numStars
@@ -537,6 +538,24 @@ class DetailsActivity :
return getString(R.string.chapters_time_pattern, this, timeFormatted)
}
private fun Manga.getAuthorsString(): SpannedString? {
if (authors.isEmpty()) {
return null
}
return buildSpannedString {
authors.forEach { a ->
if (a.isNotEmpty()) {
if (isNotEmpty()) {
append(", ")
}
inSpans(AuthorSpan(this@DetailsActivity)) {
append(a)
}
}
}
}.nullIfEmpty()
}
private class PrefetchObserver(
private val context: Context,
) : FlowCollector<List<ChapterListItem>?> {

View File

@@ -36,15 +36,15 @@ class RecoverMangaUseCase @Inject constructor(
) = Manga(
id = broken.id,
title = current.title,
altTitle = current.altTitle,
altTitles = current.altTitles,
url = current.url,
publicUrl = current.publicUrl,
rating = current.rating,
isNsfw = current.isNsfw,
contentRating = current.contentRating,
coverUrl = current.coverUrl,
tags = current.tags,
state = current.state,
author = current.author,
authors = current.authors,
largeCoverUrl = current.largeCoverUrl,
description = current.description,
chapters = current.chapters,

View File

@@ -32,7 +32,7 @@ fun mangaListDetailedItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
binding.textViewAuthor.textAndVisible = item.manga.author
binding.textViewAuthor.textAndVisible = item.manga.authors.joinToString(", ")
binding.progressView.setProgress(
value = item.progress,
animate = ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.domain.model
import android.net.Uri
import androidx.core.net.toFile
import androidx.core.net.toUri
import org.koitharu.kotatsu.core.util.ext.contains
import org.koitharu.kotatsu.core.util.ext.creationTime
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -26,8 +27,8 @@ data class LocalManga(
fun isMatchesQuery(query: String): Boolean {
return manga.title.contains(query, ignoreCase = true) ||
manga.altTitle?.contains(query, ignoreCase = true) == true ||
manga.author?.contains(query, ignoreCase = true) == true
manga.altTitles.contains(query, ignoreCase = true) ||
manga.authors.contains(query, ignoreCase = true)
}
fun containsTags(tags: Collection<String>): Boolean {

View File

@@ -8,20 +8,6 @@ fun Manga.filterChapters(branch: String?): Manga {
return withChapters(chapters = chapters?.filter { it.branch == branch })
}
private fun Manga.withChapters(chapters: List<MangaChapter>?) = Manga(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
private fun Manga.withChapters(chapters: List<MangaChapter>?) = copy(
chapters = chapters,
source = source,
)
)

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.contains
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
@@ -83,7 +84,7 @@ class SearchV2Helper @AssistedInject constructor(
}
SearchKind.AUTHOR -> retainAll { m ->
m.author.isNullOrEmpty() || m.author.equals(query, ignoreCase = true)
m.authors.isEmpty() || m.authors.contains(query, ignoreCase = true)
}
SearchKind.SIMPLE, // no filtering expected
@@ -99,7 +100,7 @@ class SearchV2Helper @AssistedInject constructor(
}
SearchKind.AUTHOR -> sortByDescending { m ->
m.author?.equals(query, ignoreCase = true) == true
m.authors.contains(query, ignoreCase = true)
}
SearchKind.TAG -> sortByDescending { m ->

View File

@@ -64,11 +64,8 @@
android:id="@+id/textView_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/custom_selectable_item_background"
android:padding="4dp"
android:singleLine="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_constrainedWidth="true"
app:layout_constraintBaseline_toBaselineOf="@id/textView_author_label"
@@ -87,7 +84,7 @@
android:text="@string/translation"
android:textAppearance="?textAppearanceTitleSmall"
app:layout_constraintStart_toStartOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/textView_author_label" />
app:layout_constraintTop_toBottomOf="@id/textView_author" />
<TextView
android:id="@+id/textView_translation"