Multiple authors support

master
Koitharu 1 year ago
parent 24cf2a2725
commit 424c4d8827
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

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

@ -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)
}
}

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

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

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

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

@ -8,20 +8,6 @@ fun Manga.filterChapters(branch: String?): Manga {
return withChapters(chapters = chapters?.filter { it.branch == branch }) return withChapters(chapters = chapters?.filter { it.branch == branch })
} }
private fun Manga.withChapters(chapters: List<MangaChapter>?) = Manga( private fun Manga.withChapters(chapters: List<MangaChapter>?) = copy(
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,
chapters = chapters, chapters = chapters,
source = source,
) )

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

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

Loading…
Cancel
Save