Fix unsupported image formats in reader
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user