Merge branch 'devel' into Instant_history
# Conflicts: # app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Date.kt # app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.ktpull/332/head
commit
fd3d6b98ee
@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
|
|
||||||
|
class LoggingAdapterDataObserver(
|
||||||
|
private val tag: String,
|
||||||
|
) : AdapterDataObserver() {
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
Log.d(tag, "onChanged()")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateRestorationPolicyChanged() {
|
||||||
|
Log.d(tag, "onStateRestorationPolicyChanged()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.util.ext
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService()
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.cache
|
|
||||||
|
|
||||||
import androidx.collection.LruCache
|
|
||||||
|
|
||||||
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize)
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.domain
|
|
||||||
|
|
||||||
class BranchComparator : Comparator<String?> {
|
|
||||||
|
|
||||||
override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2)
|
|
||||||
}
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ViewModelScoped
|
|
||||||
class MangaDetailsDelegate @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
) {
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
|
||||||
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
// Remote manga for saved and saved for remote
|
|
||||||
val relatedManga = MutableStateFlow<Manga?>(null)
|
|
||||||
val manga: StateFlow<Manga?>
|
|
||||||
get() = mangaData
|
|
||||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
|
||||||
|
|
||||||
suspend fun doLoad() {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
|
||||||
mangaData.value = manga
|
|
||||||
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
mangaData.value = manga
|
|
||||||
relatedManga.value = runCatchingCancellable {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
|
||||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)?.manga
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapChapters(
|
|
||||||
manga: Manga?,
|
|
||||||
related: Manga?,
|
|
||||||
history: MangaHistory?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chapters = manga?.chapters ?: return emptyList()
|
|
||||||
val relatedChapters = related?.chapters
|
|
||||||
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
||||||
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
} else {
|
|
||||||
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChapters(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
downloadedChapters: List<MangaChapter>?,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val result = ArrayList<ChapterListItem>(chapters.size)
|
|
||||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = chapters.size - newCount
|
|
||||||
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
|
|
||||||
for (i in chapters.indices) {
|
|
||||||
val chapter = chapters[i]
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (result.size < chapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChaptersWithSource(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
sourceChapters: List<MangaChapter>,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
|
||||||
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
|
||||||
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = sourceChapters.size - newCount
|
|
||||||
for (i in sourceChapters.indices) {
|
|
||||||
val chapter = sourceChapters[i]
|
|
||||||
val localChapter = chaptersMap.remove(chapter.id)
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += localChapter?.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
) ?: chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = true,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
|
||||||
result.ensureCapacity(result.size + chaptersMap.size)
|
|
||||||
chaptersMap.values.mapNotNullTo(result) {
|
|
||||||
if (it.branch == branch) {
|
|
||||||
it.toListItem(
|
|
||||||
isCurrent = false,
|
|
||||||
isUnread = true,
|
|
||||||
isNew = false,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.sortBy { it.chapter.number }
|
|
||||||
}
|
|
||||||
if (result.size < sourceChapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.TextView
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
|
||||||
|
|
||||||
class BranchesAdapter : BaseAdapter() {
|
|
||||||
|
|
||||||
private val dataSet = ArrayList<String?>()
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return dataSet.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return dataSet[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return dataSet[position].hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_branch, parent, false)
|
|
||||||
(view as TextView).text = dataSet[position]
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_branch_dropdown, parent, false)
|
|
||||||
(view as TextView).text = dataSet[position]
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(items: Collection<String?>) {
|
|
||||||
dataSet.replaceWith(items)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
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.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
|
|
||||||
|
|
||||||
fun chapterListItemAD(
|
|
||||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
|
||||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
|
|
||||||
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(com.google.android.material.R.attr.colorOnTertiary))
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
object LoadingFooter : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
fun interface OnPageSelectListener {
|
|
||||||
|
|
||||||
fun onPageSelected(page: MangaPage)
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
data class PageThumbnail(
|
|
||||||
val number: Int,
|
|
||||||
val isCurrent: Boolean,
|
|
||||||
val repository: MangaRepository,
|
|
||||||
val page: MangaPage
|
|
||||||
)
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import coil.ImageLoader
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PagesThumbnailsSheet :
|
|
||||||
BaseBottomSheet<SheetPagesBinding>(),
|
|
||||||
OnListItemClickListener<MangaPage>,
|
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settings: AppSettings
|
|
||||||
|
|
||||||
private lateinit var thumbnails: List<PageThumbnail>
|
|
||||||
private var spanResolver: MangaListSpanResolver? = null
|
|
||||||
private var currentPageIndex = -1
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
|
|
||||||
if (pages.isNullOrEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
|
|
||||||
val repository = mangaRepositoryFactory.create(pages.first().source)
|
|
||||||
thumbnails = pages.mapIndexed { i, x ->
|
|
||||||
PageThumbnail(
|
|
||||||
number = i + 1,
|
|
||||||
isCurrent = i == currentPageIndex,
|
|
||||||
repository = repository,
|
|
||||||
page = x,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
|
||||||
return SheetPagesBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
spanResolver = MangaListSpanResolver(view.resources)
|
|
||||||
with(binding.headerBar) {
|
|
||||||
title = arguments?.getString(ARG_TITLE)
|
|
||||||
subtitle = null
|
|
||||||
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.recyclerView) {
|
|
||||||
addItemDecoration(
|
|
||||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
|
||||||
)
|
|
||||||
adapter = PageThumbnailAdapter(
|
|
||||||
dataSet = thumbnails,
|
|
||||||
coil = coil,
|
|
||||||
scope = viewLifecycleScope,
|
|
||||||
loader = pageLoader,
|
|
||||||
clickListener = this@PagesThumbnailsSheet,
|
|
||||||
)
|
|
||||||
addOnLayoutChangeListener(spanResolver)
|
|
||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
|
||||||
if (currentPageIndex > 0) {
|
|
||||||
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
|
|
||||||
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
spanResolver = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: MangaPage, view: View) {
|
|
||||||
(
|
|
||||||
(parentFragment as? OnPageSelectListener)
|
|
||||||
?: (activity as? OnPageSelectListener)
|
|
||||||
)?.run {
|
|
||||||
onPageSelected(item)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
|
||||||
if (isExpanded) {
|
|
||||||
headerBar.subtitle = resources.getQuantityString(
|
|
||||||
R.plurals.pages,
|
|
||||||
thumbnails.size,
|
|
||||||
thumbnails.size,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
headerBar.subtitle = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ARG_PAGES = "pages"
|
|
||||||
private const val ARG_TITLE = "title"
|
|
||||||
private const val ARG_CURRENT = "current"
|
|
||||||
|
|
||||||
private const val TAG = "PagesThumbnailsSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
|
|
||||||
PagesThumbnailsSheet().withArgs(3) {
|
|
||||||
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
|
|
||||||
putString(ARG_TITLE, title)
|
|
||||||
putInt(ARG_CURRENT, currentPage)
|
|
||||||
}.show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
import coil.size.Size
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
fun pageThumbnailAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>,
|
|
||||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
|
||||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
|
|
||||||
) {
|
|
||||||
var job: Job? = null
|
|
||||||
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
|
||||||
val thumbSize = Size(
|
|
||||||
width = gridWidth,
|
|
||||||
height = (gridWidth / 13f * 18f).toInt(),
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
|
|
||||||
item.page.preview?.let { url ->
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(url)
|
|
||||||
.tag(item.page.source)
|
|
||||||
.size(thumbSize)
|
|
||||||
.scale(Scale.FILL)
|
|
||||||
.allowRgb565(true)
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}?.let { drawable ->
|
|
||||||
return@withContext drawable
|
|
||||||
}
|
|
||||||
val file = loader.loadPage(item.page, force = false)
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(file)
|
|
||||||
.size(thumbSize)
|
|
||||||
.decodeRegion(0)
|
|
||||||
.allowRgb565(isLowRamDevice(context))
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
clickListener.onItemClick(item.page, itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
job?.cancel()
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
with(binding.textViewNumber) {
|
|
||||||
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
|
|
||||||
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
|
|
||||||
text = (item.number).toString()
|
|
||||||
}
|
|
||||||
job = scope.launch {
|
|
||||||
val drawable = runCatchingCancellable {
|
|
||||||
loadPageThumbnail(item)
|
|
||||||
}.getOrNull()
|
|
||||||
binding.imageViewThumb.setImageDrawable(drawable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
|
|
||||||
class PageThumbnailAdapter(
|
|
||||||
dataSet: List<PageThumbnail>,
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>
|
|
||||||
) : ListDelegationAdapter<List<PageThumbnail>>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
|
|
||||||
setItems(dataSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import coil.target.Target
|
|
||||||
|
|
||||||
class PreferenceIconTarget(
|
|
||||||
private val preference: Preference,
|
|
||||||
) : Target {
|
|
||||||
|
|
||||||
override fun onError(error: Drawable?) {
|
|
||||||
preference.icon = error
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(placeholder: Drawable?) {
|
|
||||||
preference.icon = placeholder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(result: Drawable) {
|
|
||||||
preference.icon = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
|
|
||||||
fun Insets.end(view: View): Int {
|
|
||||||
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) left else right
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Insets.start(view: View): Int {
|
|
||||||
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) right else left
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
|
||||||
|
|
||||||
internal val RecyclerView.LayoutManager?.firstVisibleItemPosition
|
|
||||||
get() = when (this) {
|
|
||||||
is LinearLayoutManager -> findFirstVisibleItemPosition()
|
|
||||||
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0]
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val RecyclerView.LayoutManager?.isLayoutReversed
|
|
||||||
get() = when (this) {
|
|
||||||
is LinearLayoutManager -> reverseLayout
|
|
||||||
is StaggeredGridLayoutManager -> reverseLayout
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class ExpiringLruCache<T>(
|
||||||
|
val maxSize: Int,
|
||||||
|
private val lifetime: Long,
|
||||||
|
private val timeUnit: TimeUnit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
||||||
|
|
||||||
|
operator fun get(key: ContentCache.Key): T? {
|
||||||
|
val value = cache.get(key) ?: return null
|
||||||
|
if (value.isExpired) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
return value.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun set(key: ContentCache.Key, value: T) {
|
||||||
|
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
cache.evictAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trimToSize(size: Int) {
|
||||||
|
cache.trimToSize(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class ExpiringValue<T>(
|
||||||
|
private val value: T,
|
||||||
|
lifetime: Long,
|
||||||
|
timeUnit: TimeUnit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val expiresAt = SystemClock.elapsedRealtime() + timeUnit.toMillis(lifetime)
|
||||||
|
|
||||||
|
val isExpired: Boolean
|
||||||
|
get() = SystemClock.elapsedRealtime() >= expiresAt
|
||||||
|
|
||||||
|
fun get(): T? = if (isExpired) null else value
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ExpiringValue<*>
|
||||||
|
|
||||||
|
if (value != other.value) return false
|
||||||
|
return expiresAt == other.expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = value?.hashCode() ?: 0
|
||||||
|
result = 31 * result + expiresAt.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
|
||||||
|
|
||||||
// Entity to model
|
// Entity to model
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue