Use page preview in reader while loading
This commit is contained in:
@@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RetainedLifecycleCoroutineScope(
|
||||
@@ -14,7 +15,9 @@ class RetainedLifecycleCoroutineScope(
|
||||
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
|
||||
|
||||
init {
|
||||
lifecycle.addOnClearedListener(this)
|
||||
launch(Dispatchers.Main.immediate) {
|
||||
lifecycle.addOnClearedListener(this@RetainedLifecycleCoroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
@@ -3,10 +3,12 @@ package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
@@ -43,6 +45,7 @@ fun pageThumbnailAD(
|
||||
size(thumbSize)
|
||||
scale(Scale.FILL)
|
||||
allowRgb565(true)
|
||||
transformations(TrimTransformation())
|
||||
decodeRegion(0)
|
||||
mangaSourceExtra(item.page.source)
|
||||
enqueueWith(coil)
|
||||
|
||||
@@ -8,9 +8,15 @@ import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import coil3.BitmapImage
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import coil3.toBitmap
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -24,6 +30,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.use
|
||||
@@ -36,9 +43,9 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
|
||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||
@@ -49,6 +56,8 @@ import org.koitharu.kotatsu.core.util.ext.isFileUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
@@ -76,13 +85,14 @@ class PageLoader @Inject constructor(
|
||||
lifecycle: ActivityRetainedLifecycle,
|
||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
private val coil: ImageLoader,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
private val downloadSlowdownDispatcher: DownloadSlowdownDispatcher,
|
||||
) {
|
||||
|
||||
val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default
|
||||
val loaderScope = lifecycle.lifecycleScope + InternalErrorHandler() + Dispatchers.Default
|
||||
|
||||
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
|
||||
private val semaphore = Semaphore(3)
|
||||
@@ -121,6 +131,41 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadPreview(page: MangaPage): ImageSource? {
|
||||
val preview = page.preview
|
||||
if (preview.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(preview)
|
||||
.mangaSourceExtra(page.source)
|
||||
.transformations(TrimTransformation())
|
||||
.build()
|
||||
return coil.execute(request).image?.toImageSource()
|
||||
}
|
||||
|
||||
fun peekPreviewSource(preview: String?): ImageSource? {
|
||||
if (preview.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
coil.memoryCache?.let { cache ->
|
||||
val key = MemoryCache.Key(preview)
|
||||
cache[key]?.image?.let {
|
||||
return if (it is BitmapImage) {
|
||||
ImageSource.cachedBitmap(it.toBitmap())
|
||||
} else {
|
||||
ImageSource.bitmap(it.toBitmap())
|
||||
}
|
||||
}
|
||||
}
|
||||
coil.diskCache?.let { cache ->
|
||||
cache.openSnapshot(preview)?.use { snapshot ->
|
||||
return ImageSource.file(snapshot.data.toFile())
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<Uri, Float> {
|
||||
var task = tasks[page.id]?.takeIf { it.isValid() }
|
||||
if (force) {
|
||||
@@ -237,7 +282,7 @@ class PageLoader @Inject constructor(
|
||||
if (!skipCache) {
|
||||
cache.get(pageUrl)?.let { return it.toUri() }
|
||||
}
|
||||
val uri = Uri.parse(pageUrl)
|
||||
val uri = pageUrl.toUri()
|
||||
return when {
|
||||
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
|
||||
uri
|
||||
@@ -264,6 +309,12 @@ class PageLoader @Inject constructor(
|
||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||
}
|
||||
|
||||
private fun Image.toImageSource(): ImageSource = if (this is BitmapImage) {
|
||||
ImageSource.cachedBitmap(toBitmap())
|
||||
} else {
|
||||
ImageSource.bitmap(toBitmap())
|
||||
}
|
||||
|
||||
private fun Deferred<Uri>.isValid(): Boolean {
|
||||
return getCompletionResultOrNull()?.map { uri ->
|
||||
uri.exists() && uri.isTargetNotEmpty()
|
||||
|
||||
@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
@@ -108,19 +107,29 @@ class PageHolderDelegate(
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
state = State.SHOWING
|
||||
error = null
|
||||
callback.onImageShowing(readerSettings)
|
||||
if (state >= State.LOADED) {
|
||||
state = State.SHOWING
|
||||
error = null
|
||||
callback.onImageShowing(readerSettings, isPreview = false)
|
||||
} else if (state == State.LOADING_WITH_PREVIEW) {
|
||||
callback.onImageShowing(readerSettings, isPreview = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageLoaded() {
|
||||
state = State.SHOWN
|
||||
error = null
|
||||
callback.onImageShown()
|
||||
if (state >= State.LOADED) {
|
||||
state = State.SHOWN
|
||||
error = null
|
||||
callback.onImageShown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
if (state < State.LOADED) {
|
||||
// ignore preview error
|
||||
return
|
||||
}
|
||||
val uri = this.uri
|
||||
error = e
|
||||
if (state == State.LOADED && e is IOException && uri != null && uri.toFileOrNull()?.exists() != false) {
|
||||
@@ -133,7 +142,7 @@ class PageHolderDelegate(
|
||||
|
||||
override fun onChanged(value: ReaderSettings) {
|
||||
if (state == State.SHOWN) {
|
||||
callback.onImageShowing(readerSettings)
|
||||
callback.onImageShowing(readerSettings, isPreview = false)
|
||||
}
|
||||
callback.onConfigChanged()
|
||||
}
|
||||
@@ -172,21 +181,25 @@ class PageHolderDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
yield()
|
||||
launch {
|
||||
val preview = loader.loadPreview(data) ?: return@launch
|
||||
if (state == State.LOADING) {
|
||||
state = State.LOADING_WITH_PREVIEW
|
||||
callback.onPreviewReady(preview)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val task = withContext(Dispatchers.Default) {
|
||||
loader.loadPageAsync(data, force)
|
||||
}
|
||||
uri = coroutineScope {
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancelAndJoin()
|
||||
file
|
||||
}
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancelAndJoin()
|
||||
uri = file
|
||||
state = State.LOADED
|
||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(checkNotNull(uri))
|
||||
@@ -223,7 +236,7 @@ class PageHolderDelegate(
|
||||
}
|
||||
|
||||
enum class State {
|
||||
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
||||
EMPTY, LOADING, LOADING_WITH_PREVIEW, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
@@ -232,9 +245,11 @@ class PageHolderDelegate(
|
||||
|
||||
fun onError(e: Throwable)
|
||||
|
||||
fun onPreviewReady(source: ImageSource)
|
||||
|
||||
fun onImageReady(source: ImageSource)
|
||||
|
||||
fun onImageShowing(settings: ReaderSettings)
|
||||
fun onImageShowing(settings: ReaderSettings, isPreview: Boolean)
|
||||
|
||||
fun onImageShown()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class DoublePageHolder(
|
||||
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
with(binding.ssiv) {
|
||||
maxScale = 2f * maxOf(
|
||||
width / sWidth.toFloat(),
|
||||
|
||||
@@ -27,7 +27,7 @@ class ReversedPageHolder(
|
||||
.gravity = Gravity.START or Gravity.BOTTOM
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
with(binding.ssiv) {
|
||||
maxScale = 2f * maxOf(
|
||||
width / sWidth.toFloat(),
|
||||
|
||||
@@ -89,11 +89,15 @@ open class PageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
binding.ssiv.maxScale = 2f * maxOf(
|
||||
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
||||
binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
|
||||
|
||||
@@ -89,11 +89,13 @@ class WebtoonHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewReady(source: ImageSource) = Unit
|
||||
|
||||
override fun onImageReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings) {
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
||||
with(binding.ssiv) {
|
||||
scrollTo(
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:max="100" />
|
||||
android:max="100"
|
||||
app:hideAnimationBehavior="escape"
|
||||
app:showAnimationBehavior="outward" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_error"
|
||||
|
||||
@@ -31,11 +31,11 @@ material = "1.13.0-alpha11"
|
||||
moshi = "1.15.2"
|
||||
okhttp = "4.12.0"
|
||||
okio = "3.10.2"
|
||||
parsers = "bebc615376"
|
||||
parsers = "5fa7590550"
|
||||
preference = "1.2.1"
|
||||
recyclerview = "1.4.0"
|
||||
room = "2.6.1"
|
||||
ssiv = "ba48c29803"
|
||||
ssiv = "9a67b6a7c9"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
testRules = "1.6.1"
|
||||
testRunner = "1.6.2"
|
||||
|
||||
Reference in New Issue
Block a user