Update chapters list, show already downloaded chapters #95

This commit is contained in:
Koitharu
2022-01-28 19:16:33 +02:00
parent c7dc05be5a
commit 5758eed77b
16 changed files with 220 additions and 184 deletions

View File

@@ -162,7 +162,7 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
url = id,
scanlator = relations["scanlation_group"]?.getStringOrNull("name"),
uploadDate = dateFormat.tryParse(attrs.getString("publishAt")),
branch = locale.displayName.toTitleCase(locale),
branch = locale.getDisplayName(locale).toTitleCase(locale),
source = source,
)
}

View File

@@ -9,8 +9,8 @@ import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -51,12 +51,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
chaptersAdapter = ChaptersAdapter(this)
selectionDecoration = ChaptersSelectionDecoration(view.context)
with(binding.recyclerViewChapters) {
addItemDecoration(
DividerItemDecoration(
view.context,
RecyclerView.VERTICAL
)
)
addItemDecoration(MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL))
addItemDecoration(selectionDecoration!!)
setHasFixedSize(true)
adapter = chaptersAdapter
@@ -117,7 +112,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
}
return
}
if (item.isMissing) {
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
return
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
@@ -18,15 +19,14 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.toTitleCase
import java.io.IOException
import java.util.*
class DetailsViewModel(
intent: MangaIntent,
@@ -60,16 +60,6 @@ class DetailsViewModel(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val remoteManga = MutableStateFlow<Manga?>(null)
/*private val remoteManga = mangaData.mapLatest {
if (it?.source == MangaSource.LOCAL) {
runCatching {
val m = localMangaRepository.getRemoteManga(it) ?: return@mapLatest null
MangaRepository(m.source).getDetails(m)
}.getOrNull()
} else {
null
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)*/
private val chaptersReversed = settings.observe()
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
@@ -109,10 +99,10 @@ class DetailsViewModel(
selectedBranch
) { chapters, sourceManga, currentId, newCount, branch ->
val sourceChapters = sourceManga?.chapters
if (sourceChapters.isNullOrEmpty()) {
mapChapters(chapters, currentId, newCount, branch)
} else {
if (sourceManga?.source != MangaSource.LOCAL && !sourceChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
} else {
mapChapters(chapters, sourceChapters, currentId, newCount, branch)
}
}.combine(chaptersReversed) { list, reversed ->
if (reversed) list.asReversed() else list
@@ -132,12 +122,14 @@ class DetailsViewModel(
predictBranch(manga.chapters)
}
mangaData.value = manga
if (manga.source == MangaSource.LOCAL) {
remoteManga.value = runCatching {
remoteManga.value = runCatching {
if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
MangaRepository(m.source).getDetails(m)
}.getOrNull()
}
} else {
localMangaRepository.findSavedManga(manga)
}
}.getOrNull()
}
}
@@ -166,6 +158,7 @@ class DetailsViewModel(
private fun mapChapters(
chapters: List<MangaChapter>,
downloadedChapters: List<MangaChapter>?,
currentId: Long?,
newCount: Int,
branch: String?,
@@ -174,19 +167,18 @@ class DetailsViewModel(
val dateFormat = settings.dateFormat()
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val downloadedIds = downloadedChapters?.mapToSet { it.id }
for (i in chapters.indices) {
val chapter = chapters[i]
if (chapter.branch != branch) {
continue
}
result += chapter.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = downloadedIds?.contains(chapter.id) == true,
dateFormat = dateFormat,
)
}
@@ -212,29 +204,32 @@ class DetailsViewModel(
}
val localChapter = chaptersMap.remove(chapter.id)
result += localChapter?.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = false,
dateFormat = dateFormat,
) ?: chapter.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = true,
isDownloaded = false,
dateFormat = dateFormat,
)
}
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
result.ensureCapacity(result.size + chaptersMap.size)
chaptersMap.values.mapTo(result) {
it.toListItem(ChapterExtra.UNREAD, false, dateFormat)
it.toListItem(
isCurrent = false,
isUnread = true,
isNew = false,
isMissing = false,
isDownloaded = false,
dateFormat = dateFormat,
)
}
result.sortBy { it.chapter.number }
}
@@ -246,14 +241,15 @@ class DetailsViewModel(
return null
}
val groups = chapters.groupBy { it.branch }
val locale = Locale.getDefault()
var language = locale.displayLanguage.toTitleCase(locale)
if (groups.containsKey(language)) {
return language
}
language = locale.displayName.toTitleCase(locale)
if (groups.containsKey(language)) {
return language
for (locale in LocaleListCompat.getAdjustedDefault()) {
var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
if (groups.containsKey(language)) {
return language
}
language = locale.getDisplayName(locale).toTitleCase(locale)
if (groups.containsKey(language)) {
return language
}
}
return groups.maxByOrNull { it.value.size }?.key
}

View File

@@ -1,11 +1,17 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.view.View
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.textAndVisible
@@ -15,37 +21,40 @@ fun chapterListItemAD(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
itemView.setOnClickListener {
clickListener.onItemClick(item, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item, it)
val eventListener = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) = clickListener.onItemClick(item, v)
override fun onLongClick(v: View) = clickListener.onItemLongClick(item, v)
}
bind {
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description()
when (item.extra) {
ChapterExtra.UNREAD -> {
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description()
}
when (item.status) {
FLAG_UNREAD -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
}
ChapterExtra.READ -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
}
ChapterExtra.CURRENT -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline_accent)
binding.textViewNumber.setTextColor(context.getThemeColor(androidx.appcompat.R.attr.colorAccent))
}
ChapterExtra.NEW -> {
FLAG_CURRENT -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
else -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
}
}
binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewDescription.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f
val isMissing = item.hasFlag(FLAG_MISSING)
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
}
}

View File

@@ -33,8 +33,8 @@ class ChaptersAdapter(
}
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
if (oldItem.extra != newItem.extra && oldItem.chapter == newItem.chapter) {
return newItem.extra
if (oldItem.flags != newItem.flags && oldItem.chapter == newItem.chapter) {
return newItem.flags
}
return null
}

View File

@@ -4,20 +4,14 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import androidx.collection.ArraySet
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val icon = ContextCompat.getDrawable(context, R.drawable.ic_check)
private val padding = context.resources.resolveDp(16)
private val bounds = Rect()
private val selection = ArraySet<Long>()
private val selection = HashSet<Long>()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
@@ -54,7 +48,6 @@ class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoratio
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
icon ?: return
canvas.save()
if (parent.clipToPadding) {
canvas.clipRect(
@@ -73,36 +66,4 @@ class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoratio
}
canvas.restore()
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
icon ?: return
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
)
} else {
left = 0
right = parent.width
}
for (child in parent.children) {
val itemId = parent.getChildItemId(child)
if (itemId in selection) {
parent.getDecoratedBoundsWithMargins(child, bounds)
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
val hh = (bounds.height() - icon.intrinsicHeight) / 2
val top: Int = bounds.top + hh
val bottom: Int = bounds.bottom - hh
icon.setBounds(right - icon.intrinsicWidth - padding, top, right - padding, bottom)
icon.draw(canvas)
}
}
canvas.restore()
}
}

View File

@@ -1,15 +1,20 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.history.domain.ChapterExtra
data class ChapterListItem(
class ChapterListItem(
val chapter: MangaChapter,
val extra: ChapterExtra,
val isMissing: Boolean,
val flags: Int,
val uploadDate: String?,
) {
val status: Int
get() = flags and MASK_STATUS
fun hasFlag(flag: Int): Boolean {
return (flags and flag) == flag
}
fun description(): CharSequence? {
val scanlator = chapter.scanlator?.takeUnless { it.isBlank() }
return when {
@@ -18,4 +23,35 @@ data class ChapterListItem(
else -> uploadDate
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ChapterListItem
if (chapter != other.chapter) return false
if (flags != other.flags) return false
if (uploadDate != other.uploadDate) return false
return true
}
override fun hashCode(): Int {
var result = chapter.hashCode()
result = 31 * result + flags
result = 31 * result + uploadDate.hashCode()
return result
}
companion object {
const val FLAG_UNREAD = 2
const val FLAG_CURRENT = 4
const val FLAG_NEW = 8
const val FLAG_MISSING = 16
const val FLAG_DOWNLOADED = 32
const val MASK_STATUS = FLAG_UNREAD or FLAG_CURRENT
}
}

View File

@@ -1,16 +1,30 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import java.text.DateFormat
fun MangaChapter.toListItem(
extra: ChapterExtra,
isCurrent: Boolean,
isUnread: Boolean,
isNew: Boolean,
isMissing: Boolean,
isDownloaded: Boolean,
dateFormat: DateFormat,
) = ChapterListItem(
chapter = this,
extra = extra,
isMissing = isMissing,
uploadDate = if (uploadDate != 0L) dateFormat.format(uploadDate) else null
)
): ChapterListItem {
var flags = 0
if (isCurrent) flags = flags or FLAG_CURRENT
if (isUnread) flags = flags or FLAG_UNREAD
if (isNew) flags = flags or FLAG_NEW
if (isMissing) flags = flags or FLAG_MISSING
if (isDownloaded) flags = flags or FLAG_DOWNLOADED
return ChapterListItem(
chapter = this,
flags = flags,
uploadDate = if (uploadDate != 0L) dateFormat.format(uploadDate) else null
)
}

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.history.domain
enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW
}

View File

@@ -19,7 +19,6 @@ import org.koitharu.kotatsu.databinding.DialogChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.withArgs
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
@@ -51,12 +50,11 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
binding.recyclerViewChapters.adapter = ChaptersAdapter(this).apply {
setItems(chapters.mapIndexed { index, chapter ->
chapter.toListItem(
when {
index < currentPosition -> ChapterExtra.READ
index == currentPosition -> ChapterExtra.CURRENT
else -> ChapterExtra.UNREAD
},
isCurrent = index == currentPosition,
isUnread = index > currentPosition,
isNew = false,
isMissing = false,
isDownloaded = false,
dateFormat = dateFormat,
)
}) {

View File

@@ -11,6 +11,12 @@ fun LocaleListCompat.toList(): List<Locale> {
return list
}
operator fun LocaleListCompat.iterator() = object : Iterator<Locale> {
private var index = 0
override fun hasNext(): Boolean = index < size()
override fun next(): Locale = get(index++)
}
inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
destination: C,
block: (Locale) -> R,

View File

@@ -8,7 +8,7 @@ class JSONIterator(private val array: JSONArray) : Iterator<JSONObject> {
private val total = array.length()
private var index = 0
override fun hasNext() = index < total - 1
override fun hasNext() = index < total
override fun next(): JSONObject = array.getJSONObject(index++)
}

View File

@@ -7,7 +7,7 @@ class JSONStringIterator(private val array: JSONArray) : Iterator<String> {
private val total = array.length()
private var index = 0
override fun hasNext() = index < total - 1
override fun hasNext() = index < total
override fun next(): String = array.getString(index++)
}