Fix unsupported image formats in reader

This commit is contained in:
Koitharu
2020-08-27 19:54:53 +03:00
parent 0726c037a4
commit b103589bba
5 changed files with 240 additions and 104 deletions

View File

@@ -1,8 +1,12 @@
package org.koitharu.kotatsu.ui.reader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.ArrayMap
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
@@ -20,6 +24,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val tasks = ArrayMap<String, Deferred<File>>()
private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>()
private val convertLock = Mutex()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
@@ -67,6 +72,21 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
}
}
suspend fun convertInPlace(file: File) {
convertLock.withLock(file) {
withContext(Dispatchers.IO) {
val image = BitmapFactory.decodeFile(file.absolutePath)
try {
file.outputStream().use { out ->
image.compress(Bitmap.CompressFormat.WEBP, 100, out)
}
} finally {
image.recycle()
}
}
}
}
override fun dispose() {
coroutineContext.cancel()
tasks.clear()

View File

@@ -0,0 +1,106 @@
package org.koitharu.kotatsu.ui.reader.base
import android.net.Uri
import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.*
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.utils.ext.launchAfter
import org.koitharu.kotatsu.utils.ext.launchInstead
import java.io.File
import java.io.IOException
class PageHolderDelegate(
private val loader: PageLoader,
private val callback: Callback
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader {
private var state = State.EMPTY
private var job: Job? = null
private var file: File? = null
fun onBind(page: MangaPage) {
doLoad(page, force = false)
}
fun retry(page: MangaPage) {
doLoad(page, force = true)
}
fun onRecycle() {
state = State.EMPTY
file = null
job?.cancel()
}
override fun onReady() {
state = State.SHOWING
callback.onImageShowing()
}
override fun onImageLoaded() {
state = State.SHOWN
callback.onImageShown()
}
override fun onImageLoadError(e: Exception) {
val file = this.file
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
job = launchAfter(job) {
state = State.CONVERTING
try {
loader.convertInPlace(file)
state = State.CONVERTED
callback.onImageReady(file.toUri())
} catch (e2: Throwable) {
e2.addSuppressed(e)
state = State.ERROR
callback.onError(e2)
}
}
} else {
state = State.ERROR
callback.onError(e)
}
}
private fun doLoad(data: MangaPage, force: Boolean) {
job = launchInstead(job) {
state = State.LOADING
callback.onLoadingStarted()
try {
val file = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}
this@PageHolderDelegate.file = file
state = State.LOADED
callback.onImageReady(file.toUri())
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
state = State.ERROR
callback.onError(e)
}
}
}
private enum class State {
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
}
interface Callback {
fun onLoadingStarted()
fun onError(e: Throwable)
fun onImageReady(uri: Uri)
fun onImageShowing()
fun onImageShown()
}
}

View File

@@ -1,79 +1,67 @@
package org.koitharu.kotatsu.ui.reader.standard
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
class PageHolder(parent: ViewGroup, loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page),
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
PageHolderDelegate.Callback, View.OnClickListener {
private var job: Job? = null
private val delegate = PageHolderDelegate(loader, this)
init {
ssiv.setOnImageEventListener(this)
button_retry.setOnClickListener {
doLoad(boundData ?: return@setOnClickListener, force = true)
}
ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener(this)
}
override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false)
delegate.onBind(data)
}
override fun onRecycled() {
job?.cancel()
delegate.onRecycle()
ssiv.recycle()
}
private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel()
job = launch {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
onError(e)
}
}
override fun onLoadingStarted() {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
}
override fun onReady() {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat())
override fun onImageReady(uri: Uri) {
ssiv.setImage(ImageSource.uri(uri))
}
override fun onImageShowing() {
ssiv.maxScale = 2f * maxOf(
ssiv.width / ssiv.sWidth.toFloat(),
ssiv.height / ssiv.sHeight.toFloat()
)
ssiv.resetScaleAndCenter()
}
override fun onImageLoadError(e: Exception) = onError(e)
override fun onImageLoaded() {
override fun onImageShown() {
progressBar.isVisible = false
}
override fun onTileLoadError(e: Exception?) = Unit
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return)
}
}
override fun onPreviewReleased() = Unit
override fun onPreviewLoadError(e: Exception?) = Unit
private fun onError(e: Throwable) {
override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false

View File

@@ -1,64 +1,80 @@
package org.koitharu.kotatsu.ui.reader.wetoon
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon),
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
PageHolderDelegate.Callback, View.OnClickListener {
private var job: Job? = null
private val delegate = PageHolderDelegate(loader, this)
private var scrollToRestore = 0
init {
ssiv.setOnImageEventListener(this)
button_retry.setOnClickListener {
doLoad(boundData ?: return@setOnClickListener, force = true)
}
ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener(this)
}
override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false)
}
private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel()
scrollToRestore = 0
job = launch {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
onError(e)
}
}
delegate.onBind(data)
}
override fun onRecycled() {
job?.cancel()
delegate.onRecycle()
ssiv.recycle()
}
override fun onLoadingStarted() {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
}
override fun onImageReady(uri: Uri) {
ssiv.setImage(ImageSource.uri(uri))
}
override fun onImageShowing() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.scrollTo(
when {
scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> ssiv.getScrollRange()
else -> 0
}
)
}
override fun onImageShown() {
progressBar.isVisible = false
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return)
}
}
override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false
}
fun getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Int) {
@@ -68,33 +84,4 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
scrollToRestore = scroll
}
}
override fun onReady() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.scrollTo(when {
scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> ssiv.getScrollRange()
else -> 0
})
}
override fun onImageLoadError(e: Exception) = onError(e)
override fun onImageLoaded() {
progressBar.isVisible = false
}
override fun onTileLoadError(e: Exception?) = Unit
override fun onPreviewReleased() = Unit
override fun onPreviewLoadError(e: Exception?) = Unit
private fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false
}
}

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.io.IOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -37,4 +40,36 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
isFirstCall = false
}
}
}
fun CoroutineScope.launchAfter(
job: Job?,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) {
try {
job?.join()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
block()
}
fun CoroutineScope.launchInstead(
job: Job?,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) {
try {
job?.cancelAndJoin()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
block()
}