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
final override fun onCreate(savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
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.getThemeColor
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.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -80,7 +81,7 @@ tailrec fun MangaSource.unwrap(): MangaSource = if (this is MangaSourceInfo) {
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()) {
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.cloudflare.CloudFlareActivity
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.MangaSourceInfo
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(
Intent(contextOrNull(), ImageActivity::class.java)
.setData(url.toUri())
.putExtra(KEY_SOURCE, source?.name),
.putExtra(KEY_SOURCE, source?.name)
.putExtra(KEY_PREVIEW, preview),
anchor?.let { scaleUpActivityOptionsOf(it) },
)
}
@@ -768,6 +770,7 @@ class AppRouter private constructor(
const val KEY_MANGA = "manga"
const val KEY_MANGA_LIST = "manga_list"
const val KEY_PAGES = "pages"
const val KEY_PREVIEW = "preview"
const val KEY_QUERY = "query"
const val KEY_READER_MODE = "reader_mode"
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 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) {
null -> context.getString(R.string.all_languages)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.data
import org.koitharu.kotatsu.core.model.getLocale
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.domain.model.LocalManga
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.nullIfEmpty
import org.koitharu.kotatsu.reader.data.filterChapters
import java.util.Locale
data class MangaDetails(
private val manga: Manga,
@@ -39,6 +41,13 @@ data class MangaDetails(
fun toManga() = manga
fun getLocale(): Locale? {
findAppropriateLocale(chapters.keys.singleOrNull())?.let {
return it
}
return manga.source.getLocale()
}
fun filterChapters(branch: String?) = MangaDetails(
manga = manga.filterChapters(branch),
localManga = localManga?.run {
@@ -69,4 +78,16 @@ data class MangaDetails(
}
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 org.koitharu.kotatsu.R
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.LocalMangaSource
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.MangaListModel
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.MangaTag
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import javax.inject.Inject
import kotlin.math.roundToInt
@@ -179,16 +182,6 @@ class DetailsActivity :
viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
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.onDownloadStarted
.filterNot { appRouter.isChapterPagesSheetShown() }
@@ -202,7 +195,7 @@ class DetailsActivity :
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) {
when (v.id) {
@@ -232,6 +225,7 @@ class DetailsActivity :
router.openImage(
url = viewModel.coverUrl.value ?: return,
source = manga.source,
preview = CoilMemoryCacheKey.from(viewBinding.imageViewCover),
anchor = v,
)
}
@@ -407,10 +401,20 @@ class DetailsActivity :
with(viewBinding) {
textViewTitle.text = manga.title
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) }
}
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
textViewAuthorLabel.isVisible = textViewAuthor.isVisible
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.model.ListModel
import java.util.Locale
data class MangaBranch(
val name: String?,
@@ -11,8 +10,6 @@ data class MangaBranch(
val isCurrent: Boolean,
) : ListModel {
val locale: Locale? by lazy(::findAppropriateLocale)
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaBranch && other.name == name
}
@@ -28,16 +25,4 @@ data class MangaBranch(
override fun toString(): String {
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()) {
continue
}
result += ListHeader(chapter.name)
result += ListHeader(chapter)
result.addAll(b)
}
if (result.isEmpty()) {

View File

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

View File

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

View File

@@ -2,8 +2,11 @@ package org.koitharu.kotatsu.list.ui.model
import android.content.Context
import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.getLocalizedTitle
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.parsers.model.MangaChapter
@ConsistentCopyVisibility
data class ListHeader private constructor(
private val textRaw: Any,
@StringRes val buttonTextRes: Int,
@@ -25,6 +28,13 @@ data class ListHeader private constructor(
badge: String? = null,
) : 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(
dateTimeAgo: DateTimeAgo,
@StringRes buttonTextRes: Int = 0,
@@ -36,6 +46,7 @@ data class ListHeader private constructor(
is CharSequence -> textRaw
is Int -> if (textRaw != 0) context.getString(textRaw) else null
is DateTimeAgo -> textRaw.format(context)
is MangaChapter -> textRaw.getLocalizedTitle(context.resources)
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.domain.DeleteLocalMangaUseCase
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.MangaPage
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
@@ -177,7 +178,7 @@ class ReaderViewModel @Inject constructor(
}.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 manga = mangaDetails.value?.toManga()

View File

@@ -74,19 +74,27 @@
tools:ignore="ContentDescription,UnusedAttribute" />
<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_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset"
android:background="@drawable/bg_chip"
android:backgroundTint="@color/warning"
android:gravity="center"
android:paddingHorizontal="4dp"
android:paddingVertical="2dp"
android:backgroundTint="@color/nsfw_16"
android:text="@string/nsfw_16"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
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:textAlignment="center"
android:textAppearance="?textAppearanceLabelMedium"
android:textColor="?colorOnError"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />

View File

@@ -66,19 +66,27 @@
tools:ignore="ContentDescription,UnusedAttribute" />
<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_height="wrap_content"
android:layout_margin="@dimen/card_indicator_offset"
android:background="@drawable/bg_chip"
android:backgroundTint="@color/warning"
android:gravity="center"
android:paddingHorizontal="4dp"
android:paddingVertical="2dp"
android:backgroundTint="@color/nsfw_16"
android:text="@string/nsfw_16"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
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:textAlignment="center"
android:textAppearance="?textAppearanceLabelMedium"
android:textColor="?colorOnError"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@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_red">#E57373</color>
<color name="dim2">#C8000000</color>
<color name="nsfw_18">#BF360C</color>
<color name="nsfw_16">#FF6F00</color>
<!-- Color schemes colors -->
<color name="background_miku">#191C1C</color>

View File

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

View File

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

View File

@@ -229,6 +229,18 @@
<item name="android:textAppearance">?textAppearanceLabelMedium</item>
</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="">
<item name="colorControlHighlight">@color/selector_overlay</item>
</style>