Improve pages thumbnails sheet
This commit is contained in:
@@ -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()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,9 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
|
|||||||
binding.toolbar.subtitle = value
|
binding.toolbar.subtitle = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isExpanded: Boolean
|
||||||
|
get() = binding.dragHandle.isGone
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setBackgroundResource(R.drawable.sheet_toolbar_background)
|
setBackgroundResource(R.drawable.sheet_toolbar_background)
|
||||||
layoutTransition = LayoutTransition().apply {
|
layoutTransition = LayoutTransition().apply {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ class BookmarksGroupAdapter(
|
|||||||
oldItem.manga.id == newItem.manga.id
|
oldItem.manga.id == newItem.manga.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ import org.koitharu.kotatsu.local.data.CbzFetcher
|
|||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
import org.koitharu.kotatsu.local.data.LocalManga
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
@@ -136,6 +138,7 @@ interface AppModule {
|
|||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
okHttpClient: OkHttpClient,
|
okHttpClient: OkHttpClient,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
pagesCache: PagesCache,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val httpClientFactory = {
|
val httpClientFactory = {
|
||||||
okHttpClient.newBuilder()
|
okHttpClient.newBuilder()
|
||||||
@@ -162,6 +165,7 @@ interface AppModule {
|
|||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
|
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||||
import org.koitharu.kotatsu.utils.ViewBadge
|
import org.koitharu.kotatsu.utils.ViewBadge
|
||||||
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
@@ -158,14 +159,29 @@ class DetailsActivity :
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
R.id.action_incognito -> {
|
R.id.action_incognito -> {
|
||||||
openReader(isIncognitoMode = true)
|
openReader(isIncognitoMode = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_pages_thumbs -> {
|
||||||
|
val history = viewModel.historyInfo.value?.history
|
||||||
|
PagesThumbnailsSheet.show(
|
||||||
|
fm = supportFragmentManager,
|
||||||
|
manga = viewModel.manga.value ?: return false,
|
||||||
|
chapterId = history?.chapterId
|
||||||
|
?: viewModel.chapters.value?.firstOrNull()?.chapter?.id
|
||||||
|
?: return false,
|
||||||
|
currentPage = history?.page ?: 0,
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
|||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
fun listHeaderAD(
|
fun listHeaderAD(
|
||||||
listener: ListHeaderClickListener,
|
listener: ListHeaderClickListener?,
|
||||||
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
|
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
|
||||||
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
|
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
if (listener != null) {
|
||||||
binding.buttonMore.setOnClickListener {
|
binding.buttonMore.setOnClickListener {
|
||||||
listener.onListHeaderClick(item, it)
|
listener.onListHeaderClick(item, it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.getText(context)
|
binding.textViewTitle.text = item.getText(context)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||||
@@ -60,6 +61,10 @@ open class MangaListAdapter(
|
|||||||
oldItem.dateTimeAgo == newItem.dateTimeAgo
|
oldItem.dateTimeAgo == newItem.dateTimeAgo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.local.data
|
package org.koitharu.kotatsu.local.data
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileFilter
|
import java.io.FileFilter
|
||||||
import java.io.FilenameFilter
|
import java.io.FilenameFilter
|
||||||
@@ -21,5 +22,10 @@ class CbzFilter : FileFilter, FilenameFilter {
|
|||||||
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
|
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
|
||||||
return ext == "cbz" || ext == "zip"
|
return ext == "cbz" || ext == "zip"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isUriSupported(uri: Uri): Boolean {
|
||||||
|
val scheme = uri.scheme?.lowercase(Locale.ROOT)
|
||||||
|
return scheme != null && scheme == "cbz" || scheme == "zip"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ class ChaptersLoader @Inject constructor(
|
|||||||
return chapterPages.size(chapterId)
|
return chapterPages.size(chapterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun last() = chapterPages.last()
|
||||||
|
|
||||||
|
fun first() = chapterPages.first()
|
||||||
|
|
||||||
fun snapshot() = chapterPages.toList()
|
fun snapshot() = chapterPages.toList()
|
||||||
|
|
||||||
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
|
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -42,9 +43,6 @@ import javax.inject.Inject
|
|||||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
private const val PROGRESS_UNDEFINED = -1f
|
|
||||||
private const val PREFETCH_LIMIT_DEFAULT = 10
|
|
||||||
|
|
||||||
@ActivityRetainedScoped
|
@ActivityRetainedScoped
|
||||||
class PageLoader @Inject constructor(
|
class PageLoader @Inject constructor(
|
||||||
lifecycle: ActivityRetainedLifecycle,
|
lifecycle: ActivityRetainedLifecycle,
|
||||||
@@ -179,7 +177,7 @@ class PageLoader @Inject constructor(
|
|||||||
val pageUrl = getPageUrl(page)
|
val pageUrl = getPageUrl(page)
|
||||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
||||||
val uri = Uri.parse(pageUrl)
|
val uri = Uri.parse(pageUrl)
|
||||||
return if (uri.scheme == "cbz") {
|
return if (CbzFilter.isUriSupported(uri)) {
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
ZipFile(uri.schemeSpecificPart)
|
ZipFile(uri.schemeSpecificPart)
|
||||||
}.use { zip ->
|
}.use { zip ->
|
||||||
@@ -191,13 +189,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val request = Request.Builder()
|
val request = createPageRequest(page, pageUrl)
|
||||||
.url(pageUrl)
|
|
||||||
.get()
|
|
||||||
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
|
|
||||||
.tag(MangaSource::class.java, page.source)
|
|
||||||
.build()
|
|
||||||
okHttp.newCall(request).await().use { response ->
|
okHttp.newCall(request).await().use { response ->
|
||||||
check(response.isSuccessful) {
|
check(response.isSuccessful) {
|
||||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||||
@@ -218,6 +210,19 @@ class PageLoader @Inject constructor(
|
|||||||
override fun handleException(context: CoroutineContext, exception: Throwable) {
|
override fun handleException(context: CoroutineContext, exception: Throwable) {
|
||||||
exception.printStackTraceDebug()
|
exception.printStackTraceDebug()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val PROGRESS_UNDEFINED = -1f
|
||||||
|
private const val PREFETCH_LIMIT_DEFAULT = 10
|
||||||
|
|
||||||
|
fun createPageRequest(page: MangaPage, pageUrl: String) = Request.Builder()
|
||||||
|
.url(pageUrl)
|
||||||
|
.get()
|
||||||
|
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
||||||
|
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
|
||||||
|
.tag(MangaSource::class.java, page.source)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
|
|||||||
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigBottomSheet
|
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigBottomSheet
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||||
@@ -180,17 +180,13 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_pages_thumbs -> {
|
R.id.action_pages_thumbs -> {
|
||||||
val pages = viewModel.getCurrentChapterPages()
|
val state = viewModel.getCurrentState() ?: return false
|
||||||
if (!pages.isNullOrEmpty()) {
|
|
||||||
PagesThumbnailsSheet.show(
|
PagesThumbnailsSheet.show(
|
||||||
supportFragmentManager,
|
supportFragmentManager,
|
||||||
pages,
|
viewModel.manga ?: return false,
|
||||||
title?.toString().orEmpty(),
|
state.chapterId,
|
||||||
readerManager.currentReader?.getCurrentState()?.page ?: -1,
|
state.page,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_bookmark -> {
|
R.id.action_bookmark -> {
|
||||||
@@ -259,17 +255,19 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onChapterChanged(chapter: MangaChapter) {
|
override fun onChapterChanged(chapter: MangaChapter) {
|
||||||
viewModel.switchChapter(chapter.id)
|
viewModel.switchChapter(chapter.id, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageSelected(page: MangaPage) {
|
override fun onPageSelected(page: ReaderPage) {
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
val pages = viewModel.content.value?.pages ?: return@launch
|
val pages = viewModel.content.value?.pages ?: return@launch
|
||||||
val index = pages.indexOfFirst { it.id == page.id }
|
val index = pages.indexOfFirst { it.chapterId == page.chapterId && it.id == page.id }
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
readerManager.currentReader?.switchPageTo(index, true)
|
readerManager.currentReader?.switchPageTo(index, true)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.switchChapter(page.chapterId, page.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||||
|
|
||||||
class ReaderSliderListener(
|
class ReaderSliderListener(
|
||||||
@@ -41,6 +42,7 @@ class ReaderSliderListener(
|
|||||||
private fun switchPageToIndex(index: Int) {
|
private fun switchPageToIndex(index: Int) {
|
||||||
val pages = viewModel.getCurrentChapterPages()
|
val pages = viewModel.getCurrentChapterPages()
|
||||||
val page = pages?.getOrNull(index) ?: return
|
val page = pages?.getOrNull(index) ?: return
|
||||||
pageSelectListener.onPageSelected(page)
|
val chapterId = viewModel.getCurrentState()?.chapterId ?: return
|
||||||
|
pageSelectListener.onPageSelected(ReaderPage(page, index, chapterId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,13 +237,13 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}?.toMangaPage()
|
}?.toMangaPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchChapter(id: Long) {
|
fun switchChapter(id: Long, page: Int) {
|
||||||
val prevJob = loadingJob
|
val prevJob = loadingJob
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
content.postValue(ReaderContent(emptyList(), null))
|
content.postValue(ReaderContent(emptyList(), null))
|
||||||
chaptersLoader.loadSingleChapter(mangaData.requireValue(), id)
|
chaptersLoader.loadSingleChapter(mangaData.requireValue(), id)
|
||||||
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, 0, 0)))
|
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,38 +5,40 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.BoundsScrollListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.ScrollListenerInvalidationObserver
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
||||||
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
|
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.TargetScrollObserver
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.LoggingAdapterDataObserver
|
||||||
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PagesThumbnailsSheet :
|
class PagesThumbnailsSheet :
|
||||||
BaseBottomSheet<SheetPagesBinding>(),
|
BaseBottomSheet<SheetPagesBinding>(),
|
||||||
OnListItemClickListener<MangaPage>,
|
OnListItemClickListener<PageThumbnail>,
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener {
|
BottomSheetHeaderBar.OnExpansionChangeListener {
|
||||||
|
|
||||||
@Inject
|
private val viewModel by viewModels<PagesThumbnailsViewModel>()
|
||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
@@ -44,27 +46,13 @@ class PagesThumbnailsSheet :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
private lateinit var thumbnails: List<PageThumbnail>
|
private var thumbnailsAdapter: PageThumbnailAdapter? = null
|
||||||
private var spanResolver: MangaListSpanResolver? = null
|
private var spanResolver: MangaListSpanResolver? = null
|
||||||
private var currentPageIndex = -1
|
private var scrollListener: ScrollListener? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private val spanSizeLookup = SpanSizeLookup()
|
||||||
super.onCreate(savedInstanceState)
|
private val listCommitCallback = Runnable {
|
||||||
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
|
spanSizeLookup.invalidateCache()
|
||||||
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 {
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
||||||
@@ -73,74 +61,116 @@ class PagesThumbnailsSheet :
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
spanResolver = MangaListSpanResolver(view.resources)
|
spanResolver = MangaListSpanResolver(view.resources)
|
||||||
with(binding.headerBar) {
|
with(binding.headerBar) {
|
||||||
title = arguments?.getString(ARG_TITLE)
|
title = viewModel.title
|
||||||
subtitle = null
|
subtitle = null
|
||||||
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
|
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
|
||||||
}
|
}
|
||||||
|
thumbnailsAdapter = PageThumbnailAdapter(
|
||||||
|
coil = coil,
|
||||||
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
|
clickListener = this@PagesThumbnailsSheet,
|
||||||
|
)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
addItemDecoration(
|
addItemDecoration(
|
||||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
||||||
)
|
)
|
||||||
adapter = PageThumbnailAdapter(
|
adapter = thumbnailsAdapter
|
||||||
dataSet = thumbnails,
|
|
||||||
coil = coil,
|
|
||||||
scope = viewLifecycleScope,
|
|
||||||
loader = pageLoader,
|
|
||||||
clickListener = this@PagesThumbnailsSheet,
|
|
||||||
)
|
|
||||||
addOnLayoutChangeListener(spanResolver)
|
addOnLayoutChangeListener(spanResolver)
|
||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
||||||
if (currentPageIndex > 0) {
|
addOnScrollListener(ScrollListener().also { scrollListener = it })
|
||||||
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
|
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
||||||
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
|
thumbnailsAdapter?.registerAdapterDataObserver(
|
||||||
|
ScrollListenerInvalidationObserver(this, checkNotNull(scrollListener)),
|
||||||
|
)
|
||||||
|
thumbnailsAdapter?.registerAdapterDataObserver(TargetScrollObserver(this))
|
||||||
|
thumbnailsAdapter?.registerAdapterDataObserver(LoggingAdapterDataObserver("THUMB"))
|
||||||
}
|
}
|
||||||
|
viewModel.thumbnails.observe(viewLifecycleOwner) {
|
||||||
|
thumbnailsAdapter?.setItems(it, listCommitCallback)
|
||||||
}
|
}
|
||||||
|
viewModel.branch.observe(viewLifecycleOwner) {
|
||||||
|
onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded)
|
||||||
|
}
|
||||||
|
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
|
||||||
spanResolver = null
|
spanResolver = null
|
||||||
|
scrollListener = null
|
||||||
|
thumbnailsAdapter = null
|
||||||
|
spanSizeLookup.invalidateCache()
|
||||||
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MangaPage, view: View) {
|
override fun onItemClick(item: PageThumbnail, view: View) {
|
||||||
(
|
val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener)
|
||||||
(parentFragment as? OnPageSelectListener)
|
if (listener != null) {
|
||||||
?: (activity as? OnPageSelectListener)
|
listener.onPageSelected(item.page)
|
||||||
)?.run {
|
} else {
|
||||||
onPageSelected(item)
|
val state = ReaderState(item.page.chapterId, item.page.index, 0)
|
||||||
dismiss()
|
val intent = ReaderActivity.newIntent(view.context, viewModel.manga, state)
|
||||||
|
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||||
}
|
}
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
headerBar.subtitle = resources.getQuantityString(
|
headerBar.subtitle = viewModel.branch.value
|
||||||
R.plurals.pages,
|
|
||||||
thumbnails.size,
|
|
||||||
thumbnails.size,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
headerBar.subtitle = null
|
headerBar.subtitle = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class ScrollListener : BoundsScrollListener(3, 3) {
|
||||||
|
|
||||||
|
override fun onScrolledToStart(recyclerView: RecyclerView) {
|
||||||
|
viewModel.loadPrevChapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||||
|
viewModel.loadNextChapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
isSpanIndexCacheEnabled = true
|
||||||
|
isSpanGroupIndexCacheEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
val total =
|
||||||
|
(binding.recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||||
|
return when (thumbnailsAdapter?.getItemViewType(position)) {
|
||||||
|
PageThumbnailAdapter.ITEM_TYPE_THUMBNAIL -> 1
|
||||||
|
else -> total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
invalidateSpanGroupIndexCache()
|
||||||
|
invalidateSpanIndexCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ARG_PAGES = "pages"
|
const val ARG_MANGA = "manga"
|
||||||
private const val ARG_TITLE = "title"
|
const val ARG_CURRENT_PAGE = "current"
|
||||||
private const val ARG_CURRENT = "current"
|
const val ARG_CHAPTER_ID = "chapter_id"
|
||||||
|
|
||||||
private const val TAG = "PagesThumbnailsSheet"
|
private const val TAG = "PagesThumbnailsSheet"
|
||||||
|
|
||||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
|
fun show(fm: FragmentManager, manga: Manga, chapterId: Long, currentPage: Int = -1) {
|
||||||
PagesThumbnailsSheet().withArgs(3) {
|
PagesThumbnailsSheet().withArgs(3) {
|
||||||
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
|
putParcelable(ARG_MANGA, ParcelableManga(manga, true))
|
||||||
putString(ARG_TITLE, title)
|
putLong(ARG_CHAPTER_ID, chapterId)
|
||||||
putInt(ARG_CURRENT, currentPage)
|
putInt(ARG_CURRENT_PAGE, currentPage)
|
||||||
}.show(fm, TAG)
|
}.show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ class RemoteListViewModel @Inject constructor(
|
|||||||
list.toUi(this, mode, tagHighlighter)
|
list.toUi(this, mode, tagHighlighter)
|
||||||
when {
|
when {
|
||||||
error != null -> add(error.toErrorFooter())
|
error != null -> add(error.toErrorFooter())
|
||||||
hasNext -> add(LoadingFooter)
|
hasNext -> add(LoadingFooter())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
) { list, error, isHasNextPage ->
|
) { list, error, isHasNextPage ->
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
if (isHasNextPage) {
|
if (isHasNextPage) {
|
||||||
list + LoadingFooter
|
list + LoadingFooter()
|
||||||
} else {
|
} else {
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
listOf(
|
listOf(
|
||||||
when {
|
when {
|
||||||
error != null -> errorHint(error)
|
error != null -> errorHint(error)
|
||||||
isHasNextPage -> LoadingFooter
|
isHasNextPage -> LoadingFooter()
|
||||||
else -> emptyResultsHint()
|
else -> emptyResultsHint()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
@@ -34,6 +35,7 @@ class ScrobblerSelectorAdapter(
|
|||||||
oldItem === newItem -> true
|
oldItem === newItem -> true
|
||||||
oldItem is ScrobblerManga && newItem is ScrobblerManga -> oldItem.id == newItem.id
|
oldItem is ScrobblerManga && newItem is ScrobblerManga -> oldItem.id == newItem.id
|
||||||
oldItem is ScrobblerHint && newItem is ScrobblerHint -> oldItem.textPrimary == newItem.textPrimary
|
oldItem is ScrobblerHint && newItem is ScrobblerHint -> oldItem.textPrimary == newItem.textPrimary
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> oldItem.key == newItem.key
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class SearchViewModel @Inject constructor(
|
|||||||
list.toUi(result, mode, tagHighlighter)
|
list.toUi(result, mode, tagHighlighter)
|
||||||
when {
|
when {
|
||||||
error != null -> result += error.toErrorFooter()
|
error != null -> result += error.toErrorFooter()
|
||||||
hasNext -> result += LoadingFooter
|
hasNext -> result += LoadingFooter()
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class MultiSearchViewModel @Inject constructor(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
loading -> list + LoadingFooter
|
loading -> list + LoadingFooter()
|
||||||
else -> list
|
else -> list
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ class MultiSearchAdapter(
|
|||||||
oldItem.source == newItem.source
|
oldItem.source == newItem.source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
@@ -62,6 +63,10 @@ class ShelfAdapter(
|
|||||||
oldItem.key == newItem.key
|
oldItem.key == newItem.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
|
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ class FeedAdapter(
|
|||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,10 @@
|
|||||||
android:id="@+id/action_incognito"
|
android:id="@+id/action_incognito"
|
||||||
android:icon="@drawable/ic_incognito"
|
android:icon="@drawable/ic_incognito"
|
||||||
android:title="@string/incognito_mode" />
|
android:title="@string/incognito_mode" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_pages_thumbs"
|
||||||
|
android:icon="@drawable/ic_grid"
|
||||||
|
android:title="@string/pages" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
Reference in New Issue
Block a user