Improve pages thumbnails sheet
parent
43a92bdf08
commit
3f96f34b8e
@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
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()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
object LoadingFooter : ListModel {
|
class LoadingFooter @JvmOverloads constructor(
|
||||||
|
val key: Int = 0,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as LoadingFooter
|
||||||
|
|
||||||
|
return key == other.key
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.ImageSource
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.request.Options
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||||
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
class MangaPageFetcher(
|
||||||
|
private val context: Context,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
private val pagesCache: PagesCache,
|
||||||
|
private val options: Options,
|
||||||
|
private val page: MangaPage,
|
||||||
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
) : Fetcher {
|
||||||
|
|
||||||
|
override suspend fun fetch(): FetchResult {
|
||||||
|
val repo = mangaRepositoryFactory.create(page.source)
|
||||||
|
val pageUrl = repo.getPageUrl(page)
|
||||||
|
pagesCache.get(pageUrl)?.let { file ->
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(
|
||||||
|
file = file.toOkioPath(),
|
||||||
|
metadata = MangaPageMetadata(page),
|
||||||
|
),
|
||||||
|
mimeType = null,
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return loadPage(pageUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPage(pageUrl: String): SourceResult {
|
||||||
|
val uri = pageUrl.toUri()
|
||||||
|
return if (CbzFilter.isUriSupported(uri)) {
|
||||||
|
val zip = runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart) }
|
||||||
|
val entry = runInterruptible(Dispatchers.IO) { zip.getEntry(uri.fragment) }
|
||||||
|
return SourceResult(
|
||||||
|
source = ImageSource(
|
||||||
|
source = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer(),
|
||||||
|
context = context,
|
||||||
|
metadata = MangaPageMetadata(page),
|
||||||
|
),
|
||||||
|
mimeType = null,
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val request = PageLoader.createPageRequest(page, pageUrl)
|
||||||
|
okHttpClient.newCall(request).await().use { response ->
|
||||||
|
check(response.isSuccessful) {
|
||||||
|
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||||
|
}
|
||||||
|
val body = checkNotNull(response.body) {
|
||||||
|
"Null response"
|
||||||
|
}
|
||||||
|
val mimeType = response.mimeType
|
||||||
|
val file = body.use {
|
||||||
|
pagesCache.put(pageUrl, it.source())
|
||||||
|
}
|
||||||
|
SourceResult(
|
||||||
|
source = ImageSource(
|
||||||
|
file = file.toOkioPath(),
|
||||||
|
metadata = MangaPageMetadata(page),
|
||||||
|
),
|
||||||
|
mimeType = mimeType,
|
||||||
|
dataSource = DataSource.NETWORK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
private val context: Context,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
private val pagesCache: PagesCache,
|
||||||
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
) : Fetcher.Factory<MangaPage> {
|
||||||
|
|
||||||
|
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
|
return MangaPageFetcher(
|
||||||
|
okHttpClient = okHttpClient,
|
||||||
|
pagesCache = pagesCache,
|
||||||
|
options = options,
|
||||||
|
page = data,
|
||||||
|
context = context,
|
||||||
|
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata()
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
|
||||||
fun interface OnPageSelectListener {
|
fun interface OnPageSelectListener {
|
||||||
|
|
||||||
fun onPageSelected(page: MangaPage)
|
fun onPageSelected(page: ReaderPage)
|
||||||
}
|
}
|
||||||
@ -1,11 +1,34 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
|
||||||
data class PageThumbnail(
|
class PageThumbnail(
|
||||||
val number: Int,
|
|
||||||
val isCurrent: Boolean,
|
val isCurrent: Boolean,
|
||||||
val repository: MangaRepository,
|
val repository: MangaRepository,
|
||||||
val page: MangaPage
|
val page: ReaderPage,
|
||||||
)
|
) : ListModel {
|
||||||
|
|
||||||
|
val number
|
||||||
|
get() = page.index + 1
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PageThumbnail
|
||||||
|
|
||||||
|
if (isCurrent != other.isCurrent) return false
|
||||||
|
if (repository != other.repository) return false
|
||||||
|
return page == other.page
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isCurrent.hashCode()
|
||||||
|
result = 31 * result + repository.hashCode()
|
||||||
|
result = 31 * result + page.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
|
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||||
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
|
import org.koitharu.kotatsu.utils.ext.emitValue
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class PagesThumbnailsViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val chaptersLoader: ChaptersLoader,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
|
||||||
|
private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L
|
||||||
|
val manga = requireNotNull(savedStateHandle.get<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA)).manga
|
||||||
|
|
||||||
|
private val repository = mangaRepositoryFactory.create(manga.source)
|
||||||
|
private val mangaDetails = SuspendLazy {
|
||||||
|
repository.getDetails(manga).let {
|
||||||
|
chaptersLoader.chapters.clear()
|
||||||
|
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
|
||||||
|
branch.emitValue(b)
|
||||||
|
it.getChapters(b)?.forEach { ch ->
|
||||||
|
chaptersLoader.chapters.put(ch.id, ch)
|
||||||
|
}
|
||||||
|
it.filterChapters(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var loadingJob: Job? = null
|
||||||
|
private var loadingPrevJob: Job? = null
|
||||||
|
private var loadingNextJob: Job? = null
|
||||||
|
|
||||||
|
val thumbnails = MutableLiveData<List<ListModel>>()
|
||||||
|
val branch = MutableLiveData<String?>()
|
||||||
|
val title = manga.title
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadingJob = launchJob(Dispatchers.Default) {
|
||||||
|
chaptersLoader.loadSingleChapter(mangaDetails.get(), initialChapterId)
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadPrevChapter() {
|
||||||
|
if (loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingPrevJob = loadPrevNextChapter(isNext = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNextChapter() {
|
||||||
|
if (loadingJob?.isActive == true || loadingNextJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingNextJob = loadPrevNextChapter(isNext = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPrevNextChapter(isNext: Boolean): Job = launchLoadingJob(Dispatchers.Default) {
|
||||||
|
val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId
|
||||||
|
chaptersLoader.loadPrevNextChapter(mangaDetails.get(), currentId, isNext)
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateList() {
|
||||||
|
val snapshot = chaptersLoader.snapshot()
|
||||||
|
val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty()
|
||||||
|
val hasPrevChapter = snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id
|
||||||
|
val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id
|
||||||
|
val pages = buildList(snapshot.size + chaptersLoader.chapters.size() + 2) {
|
||||||
|
if (hasPrevChapter) {
|
||||||
|
add(LoadingFooter(-1))
|
||||||
|
}
|
||||||
|
var previousChapterId = 0L
|
||||||
|
for (page in snapshot) {
|
||||||
|
if (page.chapterId != previousChapterId) {
|
||||||
|
chaptersLoader.chapters[page.chapterId]?.let {
|
||||||
|
add(ListHeader(it.name, 0, null))
|
||||||
|
}
|
||||||
|
previousChapterId = page.chapterId
|
||||||
|
}
|
||||||
|
this += PageThumbnail(
|
||||||
|
isCurrent = page.chapterId == initialChapterId && page.index == currentPageIndex,
|
||||||
|
repository = repository,
|
||||||
|
page = page,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (hasNextChapter) {
|
||||||
|
add(LoadingFooter(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnails.emitValue(pages)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,91 +1,63 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
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.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||||
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
||||||
|
import org.koitharu.kotatsu.utils.ext.source
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
fun pageThumbnailAD(
|
fun pageThumbnailAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
scope: CoroutineScope,
|
lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
clickListener: OnListItemClickListener<PageThumbnail>,
|
||||||
clickListener: OnListItemClickListener<MangaPage>,
|
) = adapterDelegateViewBinding<PageThumbnail, ListModel, ItemPageThumbBinding>(
|
||||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
|
||||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
|
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
var job: Job? = null
|
|
||||||
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
||||||
val thumbSize = Size(
|
val thumbSize = Size(
|
||||||
width = gridWidth,
|
width = gridWidth,
|
||||||
height = (gridWidth / 13f * 18f).toInt(),
|
height = (gridWidth / 13f * 18f).toInt(),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
|
val clickListenerAdapter = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||||
item.page.preview?.let { url ->
|
binding.root.setOnClickListener(clickListenerAdapter)
|
||||||
coil.execute(
|
binding.root.setOnLongClickListener(clickListenerAdapter)
|
||||||
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 {
|
bind {
|
||||||
job?.cancel()
|
val data: Any = item.page.preview?.takeUnless { it.isEmpty() } ?: item.page.toMangaPage()
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
|
||||||
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
size(thumbSize)
|
||||||
|
scale(Scale.FILL)
|
||||||
|
allowRgb565(true)
|
||||||
|
decodeRegion(0)
|
||||||
|
source(item.page.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
with(binding.textViewNumber) {
|
with(binding.textViewNumber) {
|
||||||
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
|
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)
|
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
|
||||||
text = (item.number).toString()
|
text = (item.number).toString()
|
||||||
}
|
}
|
||||||
job = scope.launch {
|
|
||||||
val drawable = runCatchingCancellable {
|
|
||||||
loadPageThumbnail(item)
|
|
||||||
}.getOrNull()
|
|
||||||
binding.imageViewThumb.setImageDrawable(drawable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewRecycled {
|
onViewRecycled {
|
||||||
job?.cancel()
|
binding.imageViewThumb.disposeImageRequest()
|
||||||
job = null
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,60 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||||
|
|
||||||
class PageThumbnailAdapter(
|
class PageThumbnailAdapter(
|
||||||
dataSet: List<PageThumbnail>,
|
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
scope: CoroutineScope,
|
lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
clickListener: OnListItemClickListener<PageThumbnail>,
|
||||||
clickListener: OnListItemClickListener<MangaPage>
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||||
) : ListDelegationAdapter<List<PageThumbnail>>() {
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
|
delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener))
|
||||||
setItems(dataSet)
|
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(null))
|
||||||
|
.addDelegate(ITEM_LOADING, loadingFooterAD())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is PageThumbnail && newItem is PageThumbnail -> {
|
||||||
|
oldItem.page == newItem.page
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is ListHeader && newItem is ListHeader -> {
|
||||||
|
oldItem.textRes == newItem.textRes &&
|
||||||
|
oldItem.text == newItem.text &&
|
||||||
|
oldItem.dateTimeAgo == newItem.dateTimeAgo
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ITEM_TYPE_THUMBNAIL = 0
|
||||||
|
const val ITEM_TYPE_HEADER = 1
|
||||||
|
const val ITEM_LOADING = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||||
|
|
||||||
|
class TargetScrollObserver(
|
||||||
|
private val recyclerView: RecyclerView,
|
||||||
|
) : RecyclerView.AdapterDataObserver() {
|
||||||
|
|
||||||
|
private var isScrollToCurrentPending = true
|
||||||
|
|
||||||
|
private val layoutManager: LinearLayoutManager
|
||||||
|
get() = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
if (isScrollToCurrentPending) {
|
||||||
|
postScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postScroll() {
|
||||||
|
recyclerView.post {
|
||||||
|
scrollToTarget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToTarget() {
|
||||||
|
val adapter = recyclerView.adapter ?: return
|
||||||
|
if (recyclerView.computeVerticalScrollRange() == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val snapshot = (adapter as? AsyncListDifferDelegationAdapter<*>)?.items ?: return
|
||||||
|
val target = snapshot.indexOfFirst { it is PageThumbnail && it.isCurrent }
|
||||||
|
if (target in snapshot.indices) {
|
||||||
|
layoutManager.scrollToPositionWithOffset(target, 0)
|
||||||
|
isScrollToCurrentPending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue