Improve details activity

This commit is contained in:
Koitharu
2025-03-20 18:00:58 +02:00
parent a2f9356b8a
commit 0b8fbf892a
19 changed files with 182 additions and 67 deletions

View File

@@ -20,7 +20,7 @@ abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), Bro
private lateinit var onBackPressedCallback: WebViewBackPressedCallback private lateinit var onBackPressedCallback: WebViewBackPressedCallback
final override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return return

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.core.image
import android.os.Parcel
import android.os.Parcelable
import android.view.View
import androidx.collection.ArrayMap
import coil3.memory.MemoryCache
import coil3.request.SuccessResult
import coil3.util.CoilUtils
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
@Parcelize
class CoilMemoryCacheKey(
val data: MemoryCache.Key
) : Parcelable {
companion object : Parceler<CoilMemoryCacheKey> {
override fun CoilMemoryCacheKey.write(parcel: Parcel, flags: Int) = with(data) {
parcel.writeString(key)
parcel.writeInt(extras.size)
for (entry in extras.entries) {
parcel.writeString(entry.key)
parcel.writeString(entry.value)
}
}
override fun create(parcel: Parcel): CoilMemoryCacheKey = CoilMemoryCacheKey(
MemoryCache.Key(
key = parcel.readString().orEmpty(),
extras = run {
val size = parcel.readInt()
val map = ArrayMap<String, String>(size)
repeat(size) {
map.put(parcel.readString(), parcel.readString())
}
map
},
),
)
fun from(view: View): CoilMemoryCacheKey? {
return (CoilUtils.result(view) as? SuccessResult)?.memoryCacheKey?.let {
CoilMemoryCacheKey(it)
}
}
}
}

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.util.ext.getDisplayName import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.core.util.ext.toLocaleOrNull
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -80,7 +81,7 @@ tailrec fun MangaSource.unwrap(): MangaSource = if (this is MangaSourceInfo) {
this this
} }
fun MangaSource.getLocale(): Locale? = (unwrap() as? MangaParserSource)?.locale?.toLocale() fun MangaSource.getLocale(): Locale? = (unwrap() as? MangaParserSource)?.locale?.toLocaleOrNull()
fun MangaSource.getSummary(context: Context): String? = when (val source = unwrap()) { fun MangaSource.getSummary(context: Context): String? = when (val source = unwrap()) {
is MangaParserSource -> { is MangaParserSource -> {

View File

@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.image.CoilMemoryCacheKey
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaSourceInfo import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.model.appUrl import org.koitharu.kotatsu.core.model.appUrl
@@ -180,11 +181,12 @@ class AppRouter private constructor(
) )
} }
fun openImage(url: String, source: MangaSource?, anchor: View? = null) { fun openImage(url: String, source: MangaSource?, anchor: View? = null, preview: CoilMemoryCacheKey? = null) {
startActivity( startActivity(
Intent(contextOrNull(), ImageActivity::class.java) Intent(contextOrNull(), ImageActivity::class.java)
.setData(url.toUri()) .setData(url.toUri())
.putExtra(KEY_SOURCE, source?.name), .putExtra(KEY_SOURCE, source?.name)
.putExtra(KEY_PREVIEW, preview),
anchor?.let { scaleUpActivityOptionsOf(it) }, anchor?.let { scaleUpActivityOptionsOf(it) },
) )
} }
@@ -768,6 +770,7 @@ class AppRouter private constructor(
const val KEY_MANGA = "manga" const val KEY_MANGA = "manga"
const val KEY_MANGA_LIST = "manga_list" const val KEY_MANGA_LIST = "manga_list"
const val KEY_PAGES = "pages" const val KEY_PAGES = "pages"
const val KEY_PREVIEW = "preview"
const val KEY_QUERY = "query" const val KEY_QUERY = "query"
const val KEY_READER_MODE = "reader_mode" const val KEY_READER_MODE = "reader_mode"
const val KEY_SORT_ORDER = "sort_order" const val KEY_SORT_ORDER = "sort_order"

View File

@@ -21,7 +21,13 @@ inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException() fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
fun String.toLocale() = Locale(this) fun String.toLocale(): Locale = Locale.forLanguageTag(this)
fun String.toLocaleOrNull() = if (isEmpty()) {
null
} else {
toLocale().takeUnless { it.displayName == this }
}
fun Locale?.getDisplayName(context: Context): String = when (this) { fun Locale?.getDisplayName(context: Context): String = when (this) {
null -> context.getString(R.string.all_languages) null -> context.getString(R.string.all_languages)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.data package org.koitharu.kotatsu.details.data
import org.koitharu.kotatsu.core.model.getLocale
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -7,6 +8,7 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.data.filterChapters
import java.util.Locale
data class MangaDetails( data class MangaDetails(
private val manga: Manga, private val manga: Manga,
@@ -39,6 +41,13 @@ data class MangaDetails(
fun toManga() = manga fun toManga() = manga
fun getLocale(): Locale? {
findAppropriateLocale(chapters.keys.singleOrNull())?.let {
return it
}
return manga.source.getLocale()
}
fun filterChapters(branch: String?) = MangaDetails( fun filterChapters(branch: String?) = MangaDetails(
manga = manga.filterChapters(branch), manga = manga.filterChapters(branch),
localManga = localManga?.run { localManga = localManga?.run {
@@ -69,4 +78,16 @@ data class MangaDetails(
} }
return result return result
} }
private fun findAppropriateLocale(name: String?): Locale? {
if (name.isNullOrEmpty()) {
return null
}
return Locale.getAvailableLocales().find { lc ->
name.contains(lc.getDisplayName(lc), ignoreCase = true) ||
name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) ||
name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) ||
name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true)
}
}
} }

View File

@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.image.CoilMemoryCacheKey
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.UnknownMangaSource
@@ -100,10 +101,12 @@ import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.parsers.model.ContentRating
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
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -179,16 +182,6 @@ class DetailsActivity :
viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.tags.observe(this, ::onTagsChanged) viewModel.tags.observe(this, ::onTagsChanged)
viewModel.branches.observe(this) {
val branch = it.singleOrNull()
infoBinding.textViewTranslation.textAndVisible = branch?.name
infoBinding.textViewTranslation.drawableStart = branch?.locale?.let {
LocaleUtils.getEmojiFlag(it)
}?.let {
TextDrawable.compound(infoBinding.textViewTranslation, it)
}
infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible
}
viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted viewModel.onDownloadStarted
.filterNot { appRouter.isChapterPagesSheetShown() } .filterNot { appRouter.isChapterPagesSheetShown() }
@@ -202,7 +195,7 @@ class DetailsActivity :
addMenuProvider(menuProvider) addMenuProvider(menuProvider)
} }
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.isNsfw == true } override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.contentRating == ContentRating.ADULT }
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
@@ -232,6 +225,7 @@ class DetailsActivity :
router.openImage( router.openImage(
url = viewModel.coverUrl.value ?: return, url = viewModel.coverUrl.value ?: return,
source = manga.source, source = manga.source,
preview = CoilMemoryCacheKey.from(viewBinding.imageViewCover),
anchor = v, anchor = v,
) )
} }
@@ -407,10 +401,20 @@ class DetailsActivity :
with(viewBinding) { with(viewBinding) {
textViewTitle.text = manga.title textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitles.joinToString("\n") textViewSubtitle.textAndVisible = manga.altTitles.joinToString("\n")
textViewNsfw.isVisible = manga.isNsfw textViewNsfw16.isVisible = manga.contentRating == ContentRating.SUGGESTIVE
textViewNsfw18.isVisible = manga.contentRating == ContentRating.ADULT
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) } textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
} }
with(infoBinding) { with(infoBinding) {
val translation = details.getLocale()
infoBinding.textViewTranslation.textAndVisible = translation?.getDisplayLanguage(translation)
?.toTitleCase(translation)
infoBinding.textViewTranslation.drawableStart = translation?.let {
LocaleUtils.getEmojiFlag(it)
}?.let {
TextDrawable.compound(infoBinding.textViewTranslation, it)
}
infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible
textViewAuthor.textAndVisible = manga.author textViewAuthor.textAndVisible = manga.author
textViewAuthorLabel.isVisible = textViewAuthor.isVisible textViewAuthorLabel.isVisible = textViewAuthor.isVisible
if (manga.hasRating) { if (manga.hasRating) {

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Locale
data class MangaBranch( data class MangaBranch(
val name: String?, val name: String?,
@@ -11,8 +10,6 @@ data class MangaBranch(
val isCurrent: Boolean, val isCurrent: Boolean,
) : ListModel { ) : ListModel {
val locale: Locale? by lazy(::findAppropriateLocale)
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaBranch && other.name == name return other is MangaBranch && other.name == name
} }
@@ -28,16 +25,4 @@ data class MangaBranch(
override fun toString(): String { override fun toString(): String {
return "$name: $count" return "$name: $count"
} }
private fun findAppropriateLocale(): Locale? {
if (name.isNullOrEmpty()) {
return null
}
return Locale.getAvailableLocales().find { lc ->
name.contains(lc.getDisplayName(lc), ignoreCase = true) ||
name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) ||
name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) ||
name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true)
}
}
} }

View File

@@ -71,7 +71,7 @@ class BookmarksViewModel @Inject constructor(
if (b.isNullOrEmpty()) { if (b.isNullOrEmpty()) {
continue continue
} }
result += ListHeader(chapter.name) result += ListHeader(chapter)
result.addAll(b) result.addAll(b)
} }
if (result.isEmpty()) { if (result.isEmpty()) {

View File

@@ -130,7 +130,7 @@ class PagesViewModel @Inject constructor(
for (page in snapshot) { for (page in snapshot) {
if (page.chapterId != previousChapterId) { if (page.chapterId != previousChapterId) {
chaptersLoader.peekChapter(page.chapterId)?.let { chaptersLoader.peekChapter(page.chapterId)?.let {
add(ListHeader(it.name)) add(ListHeader(it))
} }
previousChapterId = page.chapterId previousChapterId = page.chapterId
} }

View File

@@ -11,21 +11,20 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil3.Image
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asDrawable
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ErrorResult import coil3.request.ErrorResult
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.SuccessResult import coil3.request.SuccessResult
import coil3.request.lifecycle import coil3.request.lifecycle
import coil3.target.ViewTarget import coil3.target.GenericViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.image.CoilMemoryCacheKey
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.core.util.ext.end
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
@@ -63,7 +63,6 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(),
setContentView(ActivityImageBinding.inflate(layoutInflater)) setContentView(ActivityImageBinding.inflate(layoutInflater))
viewBinding.buttonBack.setOnClickListener(this) viewBinding.buttonBack.setOnClickListener(this)
viewBinding.buttonMenu.setOnClickListener(this) viewBinding.buttonMenu.setOnClickListener(this)
val imageUrl = requireNotNull(intent.data)
val menuProvider = ImageMenuProvider( val menuProvider = ImageMenuProvider(
activity = this, activity = this,
@@ -74,14 +73,14 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(),
viewModel.isLoading.observe(this, ::onLoadingStateChanged) viewModel.isLoading.observe(this, ::onLoadingStateChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.root, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.root, null))
viewModel.onImageSaved.observeEvent(this, ::onImageSaved) viewModel.onImageSaved.observeEvent(this, ::onImageSaved)
loadImage(imageUrl) loadImage()
} }
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_back -> dispatchNavigateUp() R.id.button_back -> dispatchNavigateUp()
R.id.button_menu -> menuMediator.onLongClick(v) R.id.button_menu -> menuMediator.onLongClick(v)
else -> loadImage(intent.data) else -> loadImage()
} }
} }
@@ -122,10 +121,11 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(),
return insets.consumeAll(typeMask) return insets.consumeAll(typeMask)
} }
private fun loadImage(url: Uri?) { private fun loadImage() {
ImageRequest.Builder(this) ImageRequest.Builder(this)
.data(url) .data(intent.data)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCacheKey(intent.getParcelableExtraCompat<CoilMemoryCacheKey>(AppRouter.KEY_PREVIEW)?.data)
.memoryCachePolicy(CachePolicy.READ_ONLY)
.lifecycle(this) .lifecycle(this)
.listener(this) .listener(this)
.mangaSourceExtra(MangaSource(intent.getStringExtra(AppRouter.KEY_SOURCE))) .mangaSourceExtra(MangaSource(intent.getStringExtra(AppRouter.KEY_SOURCE)))
@@ -158,11 +158,13 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(),
private class SsivTarget( private class SsivTarget(
override val view: SubsamplingScaleImageView, override val view: SubsamplingScaleImageView,
) : ViewTarget<SubsamplingScaleImageView> { ) : GenericViewTarget<SubsamplingScaleImageView>() {
override fun onError(error: Image?) = setDrawable(error?.asDrawable(view.resources)) override var drawable: Drawable? = null
set(value) {
override fun onSuccess(result: Image) = setDrawable(result.asDrawable(view.resources)) field = value
setImageDrawable(value)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return (this === other) || (other is SsivTarget && view == other.view) return (this === other) || (other is SsivTarget && view == other.view)
@@ -172,7 +174,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(),
override fun toString() = "SsivTarget(view=$view)" override fun toString() = "SsivTarget(view=$view)"
private fun setDrawable(drawable: Drawable?) { private fun setImageDrawable(drawable: Drawable?) {
if (drawable != null) { if (drawable != null) {
view.setImage(ImageSource.bitmap(drawable.toBitmap())) view.setImage(ImageSource.bitmap(drawable.toBitmap()))
} else { } else {

View File

@@ -2,8 +2,11 @@ package org.koitharu.kotatsu.list.ui.model
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.getLocalizedTitle
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.parsers.model.MangaChapter
@ConsistentCopyVisibility
data class ListHeader private constructor( data class ListHeader private constructor(
private val textRaw: Any, private val textRaw: Any,
@StringRes val buttonTextRes: Int, @StringRes val buttonTextRes: Int,
@@ -25,6 +28,13 @@ data class ListHeader private constructor(
badge: String? = null, badge: String? = null,
) : this(textRaw = textRes, buttonTextRes, payload, badge) ) : this(textRaw = textRes, buttonTextRes, payload, badge)
constructor(
chapter: MangaChapter,
@StringRes buttonTextRes: Int = 0,
payload: Any? = null,
badge: String? = null,
) : this(textRaw = chapter, buttonTextRes, payload, badge)
constructor( constructor(
dateTimeAgo: DateTimeAgo, dateTimeAgo: DateTimeAgo,
@StringRes buttonTextRes: Int = 0, @StringRes buttonTextRes: Int = 0,
@@ -36,6 +46,7 @@ data class ListHeader private constructor(
is CharSequence -> textRaw is CharSequence -> textRaw
is Int -> if (textRaw != 0) context.getString(textRaw) else null is Int -> if (textRaw != 0) context.getString(textRaw) else null
is DateTimeAgo -> textRaw.format(context) is DateTimeAgo -> textRaw.format(context)
is MangaChapter -> textRaw.getLocalizedTitle(context.resources)
else -> null else -> null
} }

View File

@@ -50,6 +50,7 @@ import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_NONE
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
@@ -177,7 +178,7 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null), }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
) )
val isMangaNsfw = manga.map { it?.isNsfw == true } val isMangaNsfw = manga.map { it?.contentRating == ContentRating.ADULT }
val isBookmarkAdded = readingState.flatMapLatest { state -> val isBookmarkAdded = readingState.flatMapLatest { state ->
val manga = mangaDetails.value?.toManga() val manga = mangaDetails.value?.toManga()

View File

@@ -74,19 +74,27 @@
tools:ignore="ContentDescription,UnusedAttribute" /> tools:ignore="ContentDescription,UnusedAttribute" />
<TextView <TextView
android:id="@+id/textView_nsfw" android:id="@+id/textView_nsfw_16"
style="@style/Widget.Kotatsu.TextView.Badge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset" android:layout_margin="@dimen/card_indicator_offset"
android:background="@drawable/bg_chip" android:backgroundTint="@color/nsfw_16"
android:backgroundTint="@color/warning" android:text="@string/nsfw_16"
android:gravity="center" android:visibility="gone"
android:paddingHorizontal="4dp" app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
android:paddingVertical="2dp" app:layout_constraintEnd_toEndOf="@id/imageView_cover"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_nsfw_18"
style="@style/Widget.Kotatsu.TextView.Badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset"
android:backgroundTint="@color/nsfw_18"
android:text="@string/nsfw" android:text="@string/nsfw"
android:textAlignment="center" android:visibility="gone"
android:textAppearance="?textAppearanceLabelMedium"
android:textColor="?colorOnError"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover" app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" /> app:layout_constraintEnd_toEndOf="@id/imageView_cover" />

View File

@@ -66,19 +66,27 @@
tools:ignore="ContentDescription,UnusedAttribute" /> tools:ignore="ContentDescription,UnusedAttribute" />
<TextView <TextView
android:id="@+id/textView_nsfw" android:id="@+id/textView_nsfw_16"
style="@style/Widget.Kotatsu.TextView.Badge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset" android:layout_margin="@dimen/card_indicator_offset"
android:background="@drawable/bg_chip" android:backgroundTint="@color/nsfw_16"
android:backgroundTint="@color/warning" android:text="@string/nsfw_16"
android:gravity="center" android:visibility="gone"
android:paddingHorizontal="4dp" app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
android:paddingVertical="2dp" app:layout_constraintEnd_toEndOf="@id/imageView_cover"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_nsfw_18"
style="@style/Widget.Kotatsu.TextView.Badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset"
android:backgroundTint="@color/nsfw_18"
android:text="@string/nsfw" android:text="@string/nsfw"
android:textAlignment="center" android:visibility="gone"
android:textAppearance="?textAppearanceLabelMedium"
android:textColor="?colorOnError"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover" app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" /> app:layout_constraintEnd_toEndOf="@id/imageView_cover" />

View File

@@ -10,6 +10,8 @@
<color name="common_green">#81C784</color> <color name="common_green">#81C784</color>
<color name="common_red">#E57373</color> <color name="common_red">#E57373</color>
<color name="dim2">#C8000000</color> <color name="dim2">#C8000000</color>
<color name="nsfw_18">#BF360C</color>
<color name="nsfw_16">#FF6F00</color>
<!-- Color schemes colors --> <!-- Color schemes colors -->
<color name="background_miku">#191C1C</color> <color name="background_miku">#191C1C</color>

View File

@@ -20,6 +20,8 @@
<color name="launcher_background">#FFFFFF</color> <color name="launcher_background">#FFFFFF</color>
<color name="common_green">#388E3C</color> <color name="common_green">#388E3C</color>
<color name="common_red">#D32F2F</color> <color name="common_red">#D32F2F</color>
<color name="nsfw_18">#FF8A65</color>
<color name="nsfw_16">#FFD54F</color>
<!-- Color schemes colors --> <!-- Color schemes colors -->
<color name="background_miku">#F7FAF8</color> <color name="background_miku">#F7FAF8</color>

View File

@@ -215,6 +215,7 @@
<string name="preload_pages">Preload pages</string> <string name="preload_pages">Preload pages</string>
<string name="logged_in_as">Logged in as %s</string> <string name="logged_in_as">Logged in as %s</string>
<string name="nsfw">18+</string> <string name="nsfw">18+</string>
<string name="nsfw_16">16+</string>
<string name="various_languages">Various languages</string> <string name="various_languages">Various languages</string>
<string name="search_chapters">Find chapter</string> <string name="search_chapters">Find chapter</string>
<string name="chapters_empty">No chapters in this manga</string> <string name="chapters_empty">No chapters in this manga</string>

View File

@@ -229,6 +229,18 @@
<item name="android:textAppearance">?textAppearanceLabelMedium</item> <item name="android:textAppearance">?textAppearanceLabelMedium</item>
</style> </style>
<style name="Widget.Kotatsu.TextView.Badge" parent="Widget.MaterialComponents.TextView">
<item name="android:background">@drawable/bg_chip</item>
<item name="android:gravity">center</item>
<item name="android:textAlignment">center</item>
<item name="android:paddingStart">4dp</item>
<item name="android:paddingEnd">4dp</item>
<item name="android:paddingTop">2dp</item>
<item name="android:paddingBottom">2dp</item>
<item name="android:textAppearance">?textAppearanceLabelMedium</item>
<item name="android:textColor">?colorOnBackground</item>
</style>
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent=""> <style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
<item name="colorControlHighlight">@color/selector_overlay</item> <item name="colorControlHighlight">@color/selector_overlay</item>
</style> </style>