diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt index de38b44f9..e69127145 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt @@ -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>() private val okHttp by inject() private val cache by inject() + 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() diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/base/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/base/PageHolderDelegate.kt new file mode 100644 index 000000000..cf6153df4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/base/PageHolderDelegate.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt index 124bfe607..feab5a8da 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt @@ -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(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 diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt index 81fbba487..1522210a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt @@ -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(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 - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt index c99cf4630..f078d3180 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt @@ -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 Flow.onFirst(action: suspend (T) -> Unit): Flow { 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() } \ No newline at end of file