Fix unsupported image formats in reader

pull/26/head
Koitharu 6 years ago
parent 0726c037a4
commit b103589bba

@ -1,8 +1,12 @@
package org.koitharu.kotatsu.ui.reader package org.koitharu.kotatsu.ui.reader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.ArrayMap import android.util.ArrayMap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@ -20,6 +24,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val tasks = ArrayMap<String, Deferred<File>>() private val tasks = ArrayMap<String, Deferred<File>>()
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
private val convertLock = Mutex()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job 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() { override fun dispose() {
coroutineContext.cancel() coroutineContext.cancel()
tasks.clear() 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 package org.koitharu.kotatsu.ui.reader.standard
import android.net.Uri
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page.* import kotlinx.android.synthetic.main.item_page.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage 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.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage 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), 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 { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener { button_retry.setOnClickListener(this)
doLoad(boundData ?: return@setOnClickListener, force = true)
}
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false) delegate.onBind(data)
} }
override fun onRecycled() { override fun onRecycled() {
job?.cancel() delegate.onRecycle()
ssiv.recycle() ssiv.recycle()
} }
private fun doLoad(data: MangaPage, force: Boolean) { override fun onLoadingStarted() {
job?.cancel() layout_error.isVisible = false
job = launch { progressBar.isVisible = true
layout_error.isVisible = false ssiv.recycle()
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 onReady() { override fun onImageReady(uri: Uri) {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat()) ssiv.setImage(ImageSource.uri(uri))
ssiv.resetScaleAndCenter()
} }
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageShowing() {
ssiv.maxScale = 2f * maxOf(
ssiv.width / ssiv.sWidth.toFloat(),
ssiv.height / ssiv.sHeight.toFloat()
)
ssiv.resetScaleAndCenter()
}
override fun onImageLoaded() { override fun onImageShown() {
progressBar.isVisible = false progressBar.isVisible = false
} }
override fun onTileLoadError(e: Exception?) = Unit override fun onClick(v: View) {
when (v.id) {
override fun onPreviewReleased() = Unit R.id.button_retry -> delegate.retry(boundData ?: return)
}
override fun onPreviewLoadError(e: Exception?) = Unit }
private fun onError(e: Throwable) { override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources) textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true layout_error.isVisible = true
progressBar.isVisible = false progressBar.isVisible = false

@ -1,100 +1,87 @@
package org.koitharu.kotatsu.ui.reader.wetoon package org.koitharu.kotatsu.ui.reader.wetoon
import android.net.Uri
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page_webtoon.* import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage 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.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) : class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon), 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 private var scrollToRestore = 0
init { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener { button_retry.setOnClickListener(this)
doLoad(boundData ?: return@setOnClickListener, force = true)
}
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false) delegate.onBind(data)
}
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)
}
}
} }
override fun onRecycled() { override fun onRecycled() {
job?.cancel() delegate.onRecycle()
ssiv.recycle() ssiv.recycle()
} }
fun getScrollY() = ssiv.getScroll() override fun onLoadingStarted() {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
}
fun restoreScroll(scroll: Int) { override fun onImageReady(uri: Uri) {
if (ssiv.isReady) { ssiv.setImage(ImageSource.uri(uri))
ssiv.scrollTo(scroll)
} else {
scrollToRestore = scroll
}
} }
override fun onReady() { override fun onImageShowing() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat() ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat() ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.scrollTo(when { ssiv.scrollTo(
scrollToRestore != 0 -> scrollToRestore when {
itemView.top < 0 -> ssiv.getScrollRange() scrollToRestore != 0 -> scrollToRestore
else -> 0 itemView.top < 0 -> ssiv.getScrollRange()
}) else -> 0
}
)
} }
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageShown() {
override fun onImageLoaded() {
progressBar.isVisible = false progressBar.isVisible = false
} }
override fun onTileLoadError(e: Exception?) = Unit override fun onClick(v: View) {
when (v.id) {
override fun onPreviewReleased() = Unit R.id.button_retry -> delegate.retry(boundData ?: return)
}
override fun onPreviewLoadError(e: Exception?) = Unit }
private fun onError(e: Throwable) { override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources) textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true layout_error.isVisible = true
progressBar.isVisible = false progressBar.isVisible = false
} }
fun getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Int) {
if (ssiv.isReady) {
ssiv.scrollTo(scroll)
} else {
scrollToRestore = scroll
}
}
} }

@ -1,12 +1,15 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.io.IOException import java.io.IOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -38,3 +41,35 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
} }
} }
} }
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()
}
Loading…
Cancel
Save