diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index e31e657b13..6d516b9ec3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -208,6 +208,8 @@ class ReaderActivity : BaseActivity() { private var indexChapterToShift: Long? = null private var lastCropRes = 0 + var manuallyShiftedPages = false + private set val isSplitScreen: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode @@ -593,6 +595,7 @@ class ReaderActivity : BaseActivity() { when (item.itemId) { R.id.action_shift_double_page -> { shiftDoublePages() + manuallyShiftedPages = true } else -> return super.onOptionsItemSelected(item) } @@ -600,12 +603,13 @@ class ReaderActivity : BaseActivity() { } fun shiftDoublePages(forceShift: Boolean? = null, page: ReaderPage? = null) { - (viewer as? PagerViewer)?.config?.let { config -> - if (forceShift == config.shiftDoublePage) return - config.shiftDoublePage = !config.shiftDoublePage + (viewer as? PagerViewer)?.let { pViewer -> + if (forceShift == pViewer.config.shiftDoublePage) return + if (page != null && pViewer.getShiftedPage() == page) return + pViewer.config.shiftDoublePage = !pViewer.config.shiftDoublePage viewModel.state.value.viewerChapters?.let { - (viewer as? PagerViewer)?.updateShifting(page) - (viewer as? PagerViewer)?.setChaptersDoubleShift(it) + pViewer.updateShifting(page) + pViewer.setChaptersDoubleShift(it) invalidateOptionsMenu() } } @@ -821,6 +825,7 @@ class ReaderActivity : BaseActivity() { binding.chaptersSheet.shiftPageButton.setOnClickListener { shiftDoublePages() + manuallyShiftedPages = true } binding.readerNav.leftChapter.setOnClickListener { loadAdjacentChapter(false) } @@ -1303,6 +1308,9 @@ class ReaderActivity : BaseActivity() { binding.chaptersSheet.root.sheetBehavior.isCollapsed() -> View.VISIBLE else -> View.INVISIBLE } + if (lastShiftDoubleState == null) { + manuallyShiftedPages = false + } lastShiftDoubleState = null viewer?.setChapters(viewerChapters) intentPageNumber?.let { moveToPageIndex(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 30e37b0867..161584257d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -33,22 +33,21 @@ import eu.kanade.tachiyomi.util.system.bottomCutoutInset import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.isInNightMode +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.topCutoutInset +import eu.kanade.tachiyomi.util.system.withIOContext +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.isVisibleOnScreen import eu.kanade.tachiyomi.widget.ViewPagerAdapter +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.InputStream @@ -117,10 +116,10 @@ class PagerPageHolder( private var extraProgressJob: Job? = null /** - * Subscription used to read the header of the image. This is needed in order to instantiate - * the appropiate image view depending if the image is animated (GIF). + * Job used to read the header of the image. This is needed in order to instantiate + * the appropriate image view depending if the image is animated (GIF). */ - private var readImageHeaderSubscription: Subscription? = null + private var readImageHeaderJob: Job? = null private var status = Page.State.READY private var extraStatus = Page.State.READY @@ -197,7 +196,7 @@ class PagerPageHolder( cancelLoadJob(1) cancelProgressJob(2) cancelLoadJob(2) - unsubscribeReadImageHeader() + cancelReadImageHeader() (pageView as? SubsamplingScaleImageView)?.setOnImageEventListener(null) } @@ -442,9 +441,9 @@ class PagerPageHolder( /** * Unsubscribes from the read image header subscription. */ - private fun unsubscribeReadImageHeader() { - readImageHeaderSubscription?.unsubscribe() - readImageHeaderSubscription = null + private fun cancelReadImageHeader() { + readImageHeaderJob?.cancel() + readImageHeaderJob = null } /** @@ -487,40 +486,35 @@ class PagerPageHolder( retryButton?.isVisible = false decodeErrorLayout?.isVisible = false - unsubscribeReadImageHeader() + cancelReadImageHeader() val streamFn = page.stream ?: return val streamFn2 = extraPage?.stream var openStream: InputStream? = null - readImageHeaderSubscription = Observable - .fromCallable { + readImageHeaderJob = scope.launchIO { + try { val stream = streamFn().buffered(16) val stream2 = streamFn2?.invoke()?.buffered(16) openStream = this@PagerPageHolder.mergeOrSplitPages(stream, stream2) - ImageUtil.isAnimatedAndSupported(stream) || - if (stream2 != null) ImageUtil.isAnimatedAndSupported(stream2) else false - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { isAnimated -> - if (!isAnimated) { - if (viewer.config.readerTheme >= 2) { - if (page.bg != null && - page.bgType == getBGType(viewer.config.readerTheme, context) + item.hashCode() - ) { - setImage(openStream!!, false, imageConfig) - pageView?.background = page.bg - } - // if the user switches to automatic when pages are already cached, the bg needs to be loaded - else { - val bytesArray = openStream!!.readBytes() - val bytesStream = bytesArray.inputStream() - setImage(bytesStream, false, imageConfig) - bytesStream.close() + val isAnimated = ImageUtil.isAnimatedAndSupported(stream) || + (stream2?.let { ImageUtil.isAnimatedAndSupported(stream2) } ?: false) + withUIContext { + if (!isAnimated) { + if (viewer.config.readerTheme >= 2) { + val bgType = getBGType(viewer.config.readerTheme, context) + if (page.bg != null && page.bgType == bgType) { + setImage(openStream!!, false, imageConfig) + pageView?.background = page.bg + } + // if the user switches to automatic when pages are already cached, the bg needs to be loaded + else { + val bytesArray = openStream!!.readBytes() + val bytesStream = bytesArray.inputStream() + setImage(bytesStream, false, imageConfig) + closeStreams(bytesStream) - scope.launchUI { try { pageView?.background = setBG(bytesArray) } catch (e: Exception) { @@ -528,36 +522,26 @@ class PagerPageHolder( pageView?.background = ColorDrawable(Color.WHITE) } finally { page.bg = pageView?.background - page.bgType = getBGType( - viewer.config.readerTheme, - context, - ) + item.hashCode() + page.bgType = bgType } } + } else { + setImage(openStream!!, false, imageConfig) } } else { - setImage(openStream!!, false, imageConfig) - } - } else { - setImage(openStream!!, true, imageConfig) - if (viewer.config.readerTheme >= 2 && page.bg != null) { - pageView?.background = page.bg + setImage(openStream!!, true, imageConfig) + if (viewer.config.readerTheme >= 2 && page.bg != null) { + pageView?.background = page.bg + } } } - } - // Keep the Rx stream alive to close the input stream only when unsubscribed - .flatMap { Observable.never() } - .doOnUnsubscribe { + } catch (_: Exception) { try { - openStream?.close() - } catch (_: Exception) {} + openStream?.let { closeStreams(it) } + } catch (_: Exception) { + } } - .doOnError { - try { - openStream?.close() - } catch (_: Exception) {} - } - .subscribe({}, {}) + } } private val imageConfig: Config @@ -716,18 +700,16 @@ class PagerPageHolder( return decodeLayout } - private fun mergeOrSplitPages(imageStream: InputStream, imageStream2: InputStream?): InputStream { + private suspend fun mergeOrSplitPages(imageStream: InputStream, imageStream2: InputStream?): InputStream { if (ImageUtil.isAnimatedAndSupported(imageStream)) { - imageStream.reset() + withContext(Dispatchers.IO) { imageStream.reset() } if (page.longPage == null) { page.longPage = true if (viewer.config.splitPages || imageStream2 != null) { splitDoublePages() } } - scope.launchUI { - progressBar.completeAndFadeOut() - } + withUIContext { progressBar.completeAndFadeOut() } return imageStream } if (page.longPage == true && viewer.config.splitPages) { @@ -735,7 +717,7 @@ class PagerPageHolder( val imageBitmap = try { BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) } catch (e: Exception) { - imageStream.close() + closeStreams(imageStream) Timber.e("Cannot split page ${e.message}") return imageBytes.inputStream() } @@ -756,7 +738,7 @@ class PagerPageHolder( val imageBitmap = try { BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) } catch (e: Exception) { - imageStream.close() + closeStreams(imageStream) page.longPage = true splitDoublePages() Timber.e("Cannot split page ${e.message}") @@ -765,7 +747,7 @@ class PagerPageHolder( val height = imageBitmap.height val width = imageBitmap.width return if (height < width) { - imageStream.close() + closeStreams(imageStream) page.longPage = true splitDoublePages() val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) @@ -790,8 +772,7 @@ class PagerPageHolder( val imageBitmap = try { BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) } catch (e: Exception) { - imageStream2.close() - imageStream.close() + closeStreams(imageStream, imageStream2) page.fullPage = true splitDoublePages() Timber.e("Cannot combine pages ${e.message}") @@ -802,8 +783,7 @@ class PagerPageHolder( val width = imageBitmap.width if (height < width) { - imageStream2.close() - imageStream.close() + closeStreams(imageStream, imageStream2) val oldValue = page.fullPage page.fullPage = true delayPageUpdate { @@ -827,32 +807,37 @@ class PagerPageHolder( shiftDoublePages(false) return supportHingeIfThere(imageBytes.inputStream()) } else if ((page.isEndPage == true) && - (if (page.index == 2) !viewer.activity.isFirstPageFull() else true) + (if (page.index == 2) !viewer.activity.isFirstPageFull() else true) && + extraPage?.isEndPage != true ) { shiftDoublePages(true) + extraPage = null return supportHingeIfThere(imageBytes.inputStream()) } + } else if (!viewer.activity.manuallyShiftedPages && page.index == 0 && page.isEndPage == true) { + // if for some reason the first page should be by itself but its not, fix that + shiftDoublePages(true) + extraPage = null + return supportHingeIfThere(imageBytes.inputStream()) } val imageBytes2 = imageStream2.readBytes() val imageBitmap2 = try { BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size) } catch (e: Exception) { - imageStream2.close() - imageStream.close() + closeStreams(imageStream, imageStream2) extraPage?.fullPage = true page.isolatedPage = true splitDoublePages() Timber.e("Cannot combine pages ${e.message}") return supportHingeIfThere(imageBytes.inputStream()) } - scope.launchUI { progressBar.setProgress(97) } + withUIContext { progressBar.setProgress(97) } val height2 = imageBitmap2.height val width2 = imageBitmap2.width if (height2 < width2) { - imageStream2.close() - imageStream.close() + closeStreams(imageStream, imageStream2) extraPage?.fullPage = true page.isolatedPage = true splitDoublePages() @@ -864,13 +849,12 @@ class PagerPageHolder( Color.BLACK } - imageStream.close() - imageStream2.close() - + closeStreams(imageStream, imageStream2) if (extraPage?.index == 1 && extraPage?.isStartPage == null && extraPage?.fullPage == null) { extraPage?.isStartPage = ImageUtil.isPagePadded(imageBitmap, rightSide = isLTR) if (extraPage?.isStartPage == true) { shiftDoublePages(true) + extraPage = null return supportHingeIfThere(imageBytes.inputStream()) } } @@ -885,13 +869,13 @@ class PagerPageHolder( } } - private fun supportHingeIfThere(imageStream: InputStream): InputStream { + private suspend fun supportHingeIfThere(imageStream: InputStream): InputStream { if (viewer.config.hingeGapSize > 0 && !ImageUtil.isAnimatedAndSupported(imageStream)) { val imageBytes = imageStream.readBytes() val imageBitmap = try { BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) } catch (e: Exception) { - imageStream.close() + closeStreams(imageStream) val wasNotFullPage = page.fullPage != true page.fullPage = true if (wasNotFullPage) { @@ -917,17 +901,23 @@ class PagerPageHolder( return imageStream } - private fun shiftDoublePages(shift: Boolean) { + private suspend fun closeStreams(stream1: InputStream?, stream2: InputStream? = null) { + withContext(Dispatchers.IO) { + stream1?.close() + stream2?.close() + } + } + + private suspend fun shiftDoublePages(shift: Boolean) { delayPageUpdate { viewer.activity.shiftDoublePages(shift) } } - private fun splitDoublePages() { + private suspend fun splitDoublePages() { delayPageUpdate { viewer.splitDoublePages(page) } } - private fun delayPageUpdate(callback: () -> Unit) { - scope.launchUI { - delay(100) + private suspend fun delayPageUpdate(callback: () -> Unit) { + withUIContext { callback() if (extraPage?.fullPage == true || page.fullPage == true) { extraPage = null @@ -935,13 +925,11 @@ class PagerPageHolder( } } - companion object { - fun getBGType(readerTheme: Int, context: Context): Int { - return if (readerTheme == 3) { - if (context.isInNightMode()) 2 else 1 - } else { - 0 + (context.resources.configuration?.orientation ?: 0) * 10 - } - } + private fun getBGType(readerTheme: Int, context: Context): Int { + return if (readerTheme == 3) { + if (context.isInNightMode()) 2 else 1 + } else { + 0 + (context.resources.configuration?.orientation ?: 0) * 10 + } + item.hashCode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt index 9b313b49fe..15487cd71a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -167,12 +167,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { (oldCurrent?.first as? ReaderPage)?.firstHalf == false } else { oldCurrent?.second == current || - (current.index + 1) < ( - ( - oldCurrent?.second - ?: oldCurrent?.first - ) as? ReaderPage - )?.index ?: 0 + (current.index + 1) < + (((oldCurrent?.second ?: oldCurrent?.first) as? ReaderPage)?.index ?: 0) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index b227c1e9fb..d239169340 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -647,27 +647,29 @@ object ImageUtil { val right = width - left val paddedSide = if (rightSide) right else left val unPaddedSide = if (!rightSide) right else left - return (1 until 30).all { + return (1 until 30).count { // if all of a side is padded (the left page usually has a white padding on the right when scanned) getPixel(paddedSide, (height * (it / 30f)).roundToInt()).isWhiteOrDark(checkWhite) - } && !(1 until 50).all { + } >= 27 && !(1 until 50).all { // and if all of the other side isn't padded getPixel(unPaddedSide, (height * (it / 50f)).roundToInt()).isWhiteOrDark(checkWhite) } } private fun Bitmap.isOneSideMorePadded(rightSide: Boolean, checkWhite: Boolean): Boolean { - val middle = height / 2 - val paddedSide: (Int) -> Int = { if (rightSide) width - it * 2 else it * 2 } - val unPaddedSide: (Int) -> Int = { if (!rightSide) width - it * 2 else it * 2 } -// val pixels = IntArray(100) -// getPixels(pixels, 0, 2, paddedSide(0), 0) + val middle = (height * 0.475).roundToInt() + val middle2 = (height * 0.525).roundToInt() + val widthFactor = max(1, (width / 400f).roundToInt()) + val paddedSide: (Int) -> Int = { if (!rightSide) width - it * widthFactor else it * widthFactor } + val unPaddedSide: (Int) -> Int = { if (rightSide) width - it * widthFactor else it * widthFactor } return run stop@{ - (1 until 100).any { + (1 until 37).any { if (!getPixel(paddedSide(it), middle).isWhiteOrDark(checkWhite)) return@stop false - !getPixel(unPaddedSide(it), middle).isWhiteOrDark(checkWhite) + if (!getPixel(paddedSide(it), middle2).isWhiteOrDark(checkWhite)) return@stop false + !getPixel(unPaddedSide(it), middle).isWhiteOrDark(checkWhite) || + !getPixel(unPaddedSide(it), middle2).isWhiteOrDark(checkWhite) } - } // && getPixels() + } } private fun Int.isWhiteOrDark(checkWhite: Boolean): Boolean =