From e09ae4d3473bfe1b512af1f522bf472542264fa0 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 15:43:25 -0500 Subject: [PATCH 01/61] Avoid uncaught exceptions from OkHttp interceptors crashing entire app Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com> --- .../kanade/tachiyomi/network/NetworkHelper.kt | 2 ++ .../UncaughtExceptionInterceptor.kt | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 358048ffd908..baea53a4ad44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -6,6 +6,7 @@ import com.chuckerteam.chucker.api.ChuckerInterceptor import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor +import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient @@ -33,6 +34,7 @@ class NetworkHelper(val context: Context) { .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .callTimeout(2, TimeUnit.MINUTES) + .addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(userAgentInterceptor) .apply { if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt new file mode 100644 index 000000000000..2362b78c60a9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.network.interceptor + +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +/** + * Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal + * IOException to avoid catastrophic failure. + * + * This should be the first interceptor in the client. + * + * See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/ + */ +class UncaughtExceptionInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + return try { + chain.proceed(chain.request()) + } catch (e: Exception) { + throw IOException(e) + } + } +} From 8dda29c783d9855ecc82aa2056e6450f557c53b3 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 15:43:39 -0500 Subject: [PATCH 02/61] light cleanup in MangaPlus file --- .../eu/kanade/tachiyomi/source/online/english/MangaPlus.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt index 48e39b31d738..631e0efdf1c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt @@ -42,7 +42,7 @@ class MangaPlus : DelegatedHttpSource() { return withContext(Dispatchers.IO) { val response = network.client.newCall(request).await() if (response.code != 200) throw Exception("HTTP error ${response.code}") - val body = response.body!!.string() + val body = response.body.string() val match = titleIdRegex.find(body) val titleId = match?.groupValues?.firstOrNull()?.substringAfterLast("/") ?: error("Title not found") @@ -66,7 +66,7 @@ class MangaPlus : DelegatedHttpSource() { manga.apply { this.title = trimmedTitle }, - chapters.orEmpty(), + chapters, ) } else { null From ceb971302f3434c792f538990212ee8e2ecb3d25 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 15:45:09 -0500 Subject: [PATCH 03/61] fix preload reader page setting not reflecting --- .../eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index aeb473ba410f..0996e3527471 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter @@ -30,6 +31,7 @@ class HttpPageLoader( private val chapter: ReaderChapter, private val source: HttpSource, private val chapterCache: ChapterCache = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), ) : PageLoader() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -39,7 +41,7 @@ class HttpPageLoader( */ private val queue = PriorityBlockingQueue() - private val preloadSize = 4 + private val preloadSize = preferences.preloadSize().get() init { scope.launchIO { From a925f72a0ad0975c9097665ac3b6297213e81f9b Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 15:53:39 -0500 Subject: [PATCH 04/61] Automatically shift double pages if first page is an end page Basically if page 1 is the left page in a manga's set of pages, isolate that page and page 2 and 3 would be stitched together instead. This same logic is applied if page 2 is the right page (as page 1 could be a scanlator credits page or misc, which be hard to tell where to place it) --- .../tachiyomi/ui/reader/ReaderActivity.kt | 37 +++++----- .../tachiyomi/ui/reader/model/ReaderPage.kt | 5 ++ .../ui/reader/viewer/pager/PagerPageHolder.kt | 61 ++++++++++++----- .../ui/reader/viewer/pager/PagerViewer.kt | 12 ++++ .../reader/viewer/pager/PagerViewerAdapter.kt | 8 ++- .../kanade/tachiyomi/util/system/ImageUtil.kt | 67 ++++++++++++++++++- 6 files changed, 152 insertions(+), 38 deletions(-) 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 5af61c58c1af..74db50c98f37 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 @@ -116,6 +116,7 @@ import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.spToPx +import eu.kanade.tachiyomi.util.system.toInt import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.collapse @@ -598,8 +599,9 @@ class ReaderActivity : BaseActivity() { return true } - private fun shiftDoublePages() { + fun shiftDoublePages(forceShift: Boolean? = null) { (viewer as? PagerViewer)?.config?.let { config -> + if (forceShift == config.shiftDoublePage) return config.shiftDoublePage = !config.shiftDoublePage viewModel.state.value.viewerChapters?.let { (viewer as? PagerViewer)?.updateShifting() @@ -609,6 +611,8 @@ class ReaderActivity : BaseActivity() { } } + fun isFirstPageFull(): Boolean = viewModel.getCurrentChapter()?.pages?.get(0)?.fullPage == true + private fun popToMain() { if (fromUrl) { val intent = Intent(this, MainActivity::class.java).apply { @@ -1252,16 +1256,10 @@ class ReaderActivity : BaseActivity() { pViewer.config.splitPages = preferences.automaticSplitsPage().get() && !pViewer.config.doublePages } } - val currentChapter = viewModel.getCurrentChapter() if (doublePages) { - // If we're moving from singe to double, we want the current page to be the first page - pViewer.config.shiftDoublePage = ( - binding.readerNav.pageSeekbar.value.roundToInt() + - ( - currentChapter?.pages?.take(binding.readerNav.pageSeekbar.value.roundToInt()) - ?.count { it.fullPage == true || it.isolatedPage } ?: 0 - ) - ) % 2 != 0 + // If we're moving from single to double, we want the current page to be the first page + val currentIndex = binding.readerNav.pageSeekbar.value.roundToInt() + pViewer.config.shiftDoublePage = shouldShiftDoublePages(currentIndex) } viewModel.state.value.viewerChapters?.let { pViewer.setChaptersDoubleShift(it) @@ -1269,6 +1267,15 @@ class ReaderActivity : BaseActivity() { invalidateOptionsMenu() } + private fun shouldShiftDoublePages(currentIndex: Int): Boolean { + val currentChapter = viewModel.getCurrentChapter() + val currentPage = currentChapter?.pages?.get(currentIndex) + return (currentIndex + + (currentPage?.isEndPage == true && currentPage.fullPage != true).toInt() + + (currentChapter?.pages?.take(currentIndex)?.count { it.alonePage } ?: 0) + ) % 2 != 0 + } + /** * Called from the view model whenever a new [viewerChapters] have been set. It delegates the * method to the current viewer, but also set the subtitle on the binding.toolbar. @@ -1283,14 +1290,8 @@ class ReaderActivity : BaseActivity() { indexChapterToShift = null indexPageToShift = null } else if (lastShiftDoubleState != null) { - val currentChapter = viewerChapters.currChapter - (viewer as? PagerViewer)?.config?.shiftDoublePage = ( - currentChapter.requestedPage + - ( - currentChapter.pages?.take(currentChapter.requestedPage) - ?.count { it.fullPage == true || it.isolatedPage } ?: 0 - ) - ) % 2 != 0 + val currentIndex = viewerChapters.currChapter.requestedPage + (viewer as? PagerViewer)?.config?.shiftDoublePage = shouldShiftDoublePages(currentIndex) } val currentChapterPageCount = viewerChapters.currChapter.pages?.size ?: 1 binding.readerNav.root.visibility = when { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt index 01925ef38bd8..40599e3a543b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt @@ -17,6 +17,8 @@ open class ReaderPage( var isolatedPage: Boolean = false, var firstHalf: Boolean? = null, var longPage: Boolean? = null, + var isEndPage: Boolean? = null, + var isStartPage: Boolean? = null, ) : Page(index, url, imageUrl, null) { open lateinit var chapter: ReaderChapter @@ -29,6 +31,9 @@ open class ReaderPage( if (value == true) shiftedPage = false } + val alonePage: Boolean + get() = fullPage == true || isolatedPage + fun isFromSamePage(page: ReaderPage): Boolean = index == page.index && chapter.chapter.id == page.chapter.chapter.id } 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 a022fbdbf2bb..f41ce325bf46 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 @@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.widget.ViewPagerAdapter import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -192,7 +191,6 @@ class PagerPageHolder( /** * Called when this view is detached from the window. Unsubscribes any active subscription. */ - @SuppressLint("ClickableViewAccessibility") override fun onDetachedFromWindow() { super.onDetachedFromWindow() cancelProgressJob(1) @@ -552,12 +550,12 @@ class PagerPageHolder( .doOnUnsubscribe { try { openStream?.close() - } catch (e: Exception) {} + } catch (_: Exception) {} } .doOnError { try { openStream?.close() - } catch (e: Exception) {} + } catch (_: Exception) {} } .subscribe({}, {}) } @@ -727,7 +725,7 @@ class PagerPageHolder( splitDoublePages() } } - scope?.launchUI { + scope.launchUI { progressBar.completeAndFadeOut() } return imageStream @@ -743,7 +741,7 @@ class PagerPageHolder( } val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) return ImageUtil.splitBitmap(imageBitmap, (page.firstHalf == false).xor(!isLTR)) { - scope?.launchUI { + scope.launchUI { if (it == 100) { progressBar.completeAndFadeOut() } else { @@ -772,7 +770,7 @@ class PagerPageHolder( splitDoublePages() val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) return ImageUtil.splitBitmap(imageBitmap, !isLTR) { - scope?.launchUI { + scope.launchUI { if (it == 100) { progressBar.completeAndFadeOut() } else { @@ -799,7 +797,7 @@ class PagerPageHolder( Timber.e("Cannot combine pages ${e.message}") return supportHingeIfThere(imageBytes.inputStream()) } - scope?.launchUI { progressBar.setProgress(96) } + scope.launchUI { progressBar.setProgress(96) } val height = imageBitmap.height val width = imageBitmap.width @@ -807,9 +805,28 @@ class PagerPageHolder( imageStream2.close() imageStream.close() page.fullPage = true - splitDoublePages() + delayPageUpdate { + if (page.index == 0 && viewer.config.shiftDoublePage) { + viewer.activity.shiftDoublePages(false) + } else { + viewer.splitDoublePages(page) + } + } return supportHingeIfThere(imageBytes.inputStream()) } + val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) + if (page.index <= 2 && page.isEndPage == null && page.fullPage == null) { + page.isEndPage = ImageUtil.isPagePadded(imageBitmap, rightSide = !isLTR) + if (page.index == 1 && page.isEndPage == true && viewer.config.shiftDoublePage) { + shiftDoublePages(false) + return supportHingeIfThere(imageBytes.inputStream()) + } else if ((page.isEndPage == true) && + (if (page.index == 2) !viewer.activity.isFirstPageFull() else true) + ) { + shiftDoublePages(true) + return supportHingeIfThere(imageBytes.inputStream()) + } + } val imageBytes2 = imageStream2.readBytes() val imageBitmap2 = try { @@ -823,7 +840,7 @@ class PagerPageHolder( Timber.e("Cannot combine pages ${e.message}") return supportHingeIfThere(imageBytes.inputStream()) } - scope?.launchUI { progressBar.setProgress(97) } + scope.launchUI { progressBar.setProgress(97) } val height2 = imageBitmap2.height val width2 = imageBitmap2.width @@ -835,7 +852,6 @@ class PagerPageHolder( splitDoublePages() return supportHingeIfThere(imageBytes.inputStream()) } - val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) val bg = if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) { Color.WHITE } else { @@ -844,8 +860,16 @@ class PagerPageHolder( imageStream.close() imageStream2.close() + + if (extraPage?.index == 1 && extraPage?.isStartPage == null && extraPage?.fullPage == null) { + extraPage?.isStartPage = ImageUtil.isPagePadded(imageBitmap, rightSide = isLTR) + if (extraPage?.isStartPage == true) { + shiftDoublePages(true) + return supportHingeIfThere(imageBytes.inputStream()) + } + } return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg, viewer.config.hingeGapSize, context) { - scope?.launchUI { + scope.launchUI { if (it == 100) { progressBar.completeAndFadeOut() } else { @@ -887,11 +911,18 @@ class PagerPageHolder( return imageStream } + private fun shiftDoublePages(shift: Boolean) { + delayPageUpdate { viewer.activity.shiftDoublePages(shift) } + } + private fun splitDoublePages() { - // extraPage ?: return - scope?.launchUI { + delayPageUpdate { viewer.splitDoublePages(page) } + } + + fun delayPageUpdate(callback: () -> Unit) { + scope.launchUI { delay(100) - viewer.splitDoublePages(page) + callback() if (extraPage?.fullPage == true || page.fullPage == true) { extraPage = null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index bb8026b0ca4f..9a8f8d5fa1c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -81,6 +81,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } } + private var hasMoved = false + /** * Variable used to hold the forward pos for reader activity shared transitions * Without this var landscapezoom wont work with activity transitions @@ -99,6 +101,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { override fun onPageScrollStateChanged(state: Int) { isIdle = state == ViewPager.SCROLL_STATE_IDLE + if (!hasMoved) { + hasMoved = !isIdle + } } } @@ -295,6 +300,13 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { setChaptersInternal(chapters) pager.addOnPageChangeListener(pagerListener) // Since we removed the listener while shifting, call page change to update the ui + if (!hasMoved) { + activity.isScrollingThroughPagesOrChapters = true + chapters.currChapter.pages?.let { pages -> + moveToPage(pages[chapters.currChapter.requestedPage], false) + } + activity.isScrollingThroughPagesOrChapters = false + } onPageChange(pager.currentItem) } 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 d366a4eca640..9b313b49fe8c 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 @@ -189,8 +189,12 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { if (!viewer.config.doublePages) { // If not in double mode, set up items like before subItems.forEach { - (it as? ReaderPage)?.shiftedPage = false - (it as? ReaderPage)?.firstHalf = null + (it as? ReaderPage)?.apply { + shiftedPage = false + firstHalf = null + isEndPage = null + isStartPage = null + } } if (viewer.config.splitPages) { var itemIndex = 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 e834fc70a613..f249fb370918 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 @@ -17,6 +17,7 @@ import android.graphics.drawable.GradientDrawable import android.os.Build import android.webkit.MimeTypeMap import androidx.annotation.ColorInt +import androidx.core.graphics.ColorUtils import androidx.core.graphics.alpha import androidx.core.graphics.blue import androidx.core.graphics.green @@ -36,6 +37,7 @@ import java.net.URLConnection import kotlin.math.abs import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt object ImageUtil { @@ -280,12 +282,16 @@ object ImageUtil { } val isLandscape = context.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE if (darkBG) { - return if (!isLandscape && image.getPixel(left, bot).isWhite && image.getPixel(right, bot).isWhite) { + return if (!isLandscape && image.getPixel(left, bot).isWhite && + image.getPixel(right, bot).isWhite + ) { GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor), ) - } else if (!isLandscape && image.getPixel(left, top).isWhite && image.getPixel(right, top).isWhite) { + } else if (!isLandscape && image.getPixel(left, top).isWhite && + image.getPixel(right, top).isWhite + ) { GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel), @@ -633,11 +639,66 @@ object ImageUtil { abs(color1.blue - color2.blue) < 30 } + /** Returns if this bitmap matches what would be (if rightSide param is true) + * the single left side page, or the second page to read in a RTL book, first in an LTR book + * + * @param image: bitmap image to check + * @param rightSide: when true, check if its a single left side page, else right side + * */ + fun isPagePadded(image: Bitmap, rightSide: Boolean): Boolean { + if (image.isSidePadded(!rightSide, checkWhite = true) || + image.isSidePadded(!rightSide, checkWhite = false) + ) { + return false + } + return image.isSidePadded(rightSide, checkWhite = true) || + image.isSidePadded(rightSide, checkWhite = false) || + // if neither of the above 2 worked, + // try starting from the vert. middle and see which side has more padding + image.isOneSideMorePadded(rightSide, checkWhite = true) || + image.isOneSideMorePadded(rightSide, checkWhite = false) + } + + private fun Bitmap.isSidePadded(rightSide: Boolean, checkWhite: Boolean): Boolean { + val left = (width * 0.0275).toInt() + val right = width - left + val paddedSide = if (rightSide) right else left + val unPaddedSide = if (!rightSide) right else left + return (1 until 30).all { + // 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 { + // 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) + return run stop@{ + (1 until 100).any { + if (!getPixel(paddedSide(it), middle).isWhiteOrDark(checkWhite)) return@stop false + !getPixel(unPaddedSide(it), middle).isWhiteOrDark(checkWhite) + } + } // && getPixels() + } + + private fun Int.isWhiteOrDark(checkWhite: Boolean): Boolean = + if (checkWhite) isWhite else isDark + private val Int.isWhite: Boolean get() = red + blue + green > 740 private val Int.isDark: Boolean - get() = red < 40 && blue < 40 && green < 40 && alpha > 200 + get() { + val bgArray = FloatArray(3) + ColorUtils.colorToHSL(this, bgArray) + return red < 40 && blue < 40 && green < 40 && alpha > 200 && bgArray[1] <= 0.2f + } fun getPercentOfColor( @ColorInt color: Int, From 87fe634b1c69a4c3cd19eb238dda78d92a37d334 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 20:43:47 -0500 Subject: [PATCH 05/61] minor tweaks to automatic double shifting Making sure the 2nd page doesn't undo a shift when the first page needs it + fixes to the page landed on when switching between single and double page via settings --- .../java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt | 8 ++++++-- .../tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt | 4 +++- .../tachiyomi/ui/reader/viewer/pager/PagerViewer.kt | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) 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 74db50c98f37..ac112b08ead0 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 @@ -612,6 +612,7 @@ class ReaderActivity : BaseActivity() { } fun isFirstPageFull(): Boolean = viewModel.getCurrentChapter()?.pages?.get(0)?.fullPage == true + fun isFirstPageEnd(): Boolean = viewModel.getCurrentChapter()?.pages?.get(0)?.isEndPage == true private fun popToMain() { if (fromUrl) { @@ -1259,6 +1260,8 @@ class ReaderActivity : BaseActivity() { if (doublePages) { // If we're moving from single to double, we want the current page to be the first page val currentIndex = binding.readerNav.pageSeekbar.value.roundToInt() + viewModel.getCurrentChapter()?.requestedPage = currentIndex + pViewer.hasMoved = false pViewer.config.shiftDoublePage = shouldShiftDoublePages(currentIndex) } viewModel.state.value.viewerChapters?.let { @@ -1270,10 +1273,11 @@ class ReaderActivity : BaseActivity() { private fun shouldShiftDoublePages(currentIndex: Int): Boolean { val currentChapter = viewModel.getCurrentChapter() val currentPage = currentChapter?.pages?.get(currentIndex) - return (currentIndex + + return ( + currentIndex + (currentPage?.isEndPage == true && currentPage.fullPage != true).toInt() + (currentChapter?.pages?.take(currentIndex)?.count { it.alonePage } ?: 0) - ) % 2 != 0 + ) % 2 != 0 } /** 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 f41ce325bf46..0c4ff288dce5 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 @@ -817,7 +817,9 @@ class PagerPageHolder( val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages) if (page.index <= 2 && page.isEndPage == null && page.fullPage == null) { page.isEndPage = ImageUtil.isPagePadded(imageBitmap, rightSide = !isLTR) - if (page.index == 1 && page.isEndPage == true && viewer.config.shiftDoublePage) { + if (page.index == 1 && page.isEndPage == true && viewer.config.shiftDoublePage && + !viewer.activity.isFirstPageEnd() + ) { shiftDoublePages(false) return supportHingeIfThere(imageBytes.inputStream()) } else if ((page.isEndPage == true) && diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 9a8f8d5fa1c8..93eff0504f03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -81,7 +81,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } } - private var hasMoved = false + var hasMoved = false /** * Variable used to hold the forward pos for reader activity shared transitions From a29a14970dfaea0abe742dabf1ab4b335bc9d745 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 26 Feb 2023 20:43:57 -0500 Subject: [PATCH 06/61] clean up ImageUtil --- .../kanade/tachiyomi/util/system/ImageUtil.kt | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) 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 f249fb370918..b227c1e9fbaa 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 @@ -101,7 +101,7 @@ object ImageUtil { Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P else -> false } - } catch (e: Exception) { + } catch (_: Exception) { } return false } @@ -328,23 +328,6 @@ object ImageUtil { return ColorDrawable(backgroundColor) } - /** - * Check whether the image is a double-page spread - * @return true if the width is greater than the height - */ - fun isDoublePage(imageStream: InputStream): Boolean { - imageStream.mark(imageStream.available() + 1) - - val imageBytes = imageStream.readBytes() - - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) - - imageStream.reset() - - return options.outWidth > options.outHeight - } - fun splitBitmap( imageBitmap: Bitmap, secondHalf: Boolean, From d93f9d6a4507287e4b129a7d32974d7fc6183613 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Mon, 27 Feb 2023 02:02:48 -0500 Subject: [PATCH 07/61] More fixes to auto shifting such as when the first page is a full page and the 2nd page is the left page of a manga (so page 1 and 2 need to be by themselves) --- .../eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt | 4 ++-- .../ui/reader/viewer/pager/PagerPageHolder.kt | 10 +++++++--- .../tachiyomi/ui/reader/viewer/pager/PagerViewer.kt | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) 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 ac112b08ead0..e31e657b1354 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 @@ -599,12 +599,12 @@ class ReaderActivity : BaseActivity() { return true } - fun shiftDoublePages(forceShift: Boolean? = null) { + fun shiftDoublePages(forceShift: Boolean? = null, page: ReaderPage? = null) { (viewer as? PagerViewer)?.config?.let { config -> if (forceShift == config.shiftDoublePage) return config.shiftDoublePage = !config.shiftDoublePage viewModel.state.value.viewerChapters?.let { - (viewer as? PagerViewer)?.updateShifting() + (viewer as? PagerViewer)?.updateShifting(page) (viewer as? PagerViewer)?.setChaptersDoubleShift(it) invalidateOptionsMenu() } 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 0c4ff288dce5..30e37b08676b 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 @@ -804,10 +804,14 @@ class PagerPageHolder( if (height < width) { imageStream2.close() imageStream.close() + val oldValue = page.fullPage page.fullPage = true delayPageUpdate { - if (page.index == 0 && viewer.config.shiftDoublePage) { - viewer.activity.shiftDoublePages(false) + if (page.index == 0 && + (viewer.config.shiftDoublePage || extraPage?.isEndPage == true) && + oldValue != true + ) { + viewer.activity.shiftDoublePages(extraPage?.isEndPage == true, extraPage) } else { viewer.splitDoublePages(page) } @@ -921,7 +925,7 @@ class PagerPageHolder( delayPageUpdate { viewer.splitDoublePages(page) } } - fun delayPageUpdate(callback: () -> Unit) { + private fun delayPageUpdate(callback: () -> Unit) { scope.launchUI { delay(100) callback() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 93eff0504f03..150cd318c608 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -298,8 +298,6 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { // If we don't the size change could put us on a new chapter pager.removeOnPageChangeListener(pagerListener) setChaptersInternal(chapters) - pager.addOnPageChangeListener(pagerListener) - // Since we removed the listener while shifting, call page change to update the ui if (!hasMoved) { activity.isScrollingThroughPagesOrChapters = true chapters.currChapter.pages?.let { pages -> @@ -307,6 +305,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } activity.isScrollingThroughPagesOrChapters = false } + pager.addOnPageChangeListener(pagerListener) + // Since we removed the listener while shifting, call page change to update the ui onPageChange(pager.currentItem) } From 6872616db593f9e92b18c38e5d739f8548628ef4 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Tue, 28 Feb 2023 02:27:46 -0500 Subject: [PATCH 08/61] replace subscription with job in PagerPageHolder also more fixes to the auto shifting --- .../tachiyomi/ui/reader/ReaderActivity.kt | 18 +- .../ui/reader/viewer/pager/PagerPageHolder.kt | 178 ++++++++---------- .../reader/viewer/pager/PagerViewerAdapter.kt | 8 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 22 ++- 4 files changed, 110 insertions(+), 116 deletions(-) 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 e31e657b1354..6d516b9ec3e6 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 30e37b08676b..161584257d51 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() - - scope.launchUI { + 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) + 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) {} - } - .doOnError { - try { - openStream?.close() - } catch (_: Exception) {} + openStream?.let { closeStreams(it) } + } 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 9b313b49fe8c..15487cd71a41 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 b227c1e9fbaa..d239169340ea 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 = From 4b523f6b9e23f2a274ab9fb2d66b4717917264dd Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Tue, 28 Feb 2023 16:30:46 -0500 Subject: [PATCH 09/61] clean up import in PagerPageHolder --- .../kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt | 1 - 1 file changed, 1 deletion(-) 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 161584257d51..140a1c26cb3c 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 @@ -36,7 +36,6 @@ 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 From 953f3c48f0bd6f69d59146b0aa3412978fcad826 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Tue, 28 Feb 2023 16:30:35 -0500 Subject: [PATCH 10/61] Open updates when tapping on an empty space on the widget --- .../kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt | 2 +- .../tachiyomi/appwidget/components/UpdatesWidget.kt | 8 +++++--- .../main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt index 4eac09374c37..20020b6c16be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt @@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.appwidget.components.CoverHeight import eu.kanade.tachiyomi.appwidget.components.CoverWidth import eu.kanade.tachiyomi.appwidget.components.LockedWidget +import eu.kanade.tachiyomi.appwidget.components.UpdatesWidget import eu.kanade.tachiyomi.appwidget.util.appWidgetBackgroundRadius import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount import eu.kanade.tachiyomi.data.database.models.Manga @@ -33,7 +34,6 @@ import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.launchIO import kotlinx.coroutines.MainScope -import tachiyomi.presentation.widget.components.UpdatesWidget import uy.kohesive.injekt.injectLazy import java.util.Calendar import java.util.Date diff --git a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt index a347285d2b3e..75ca31bb6246 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt @@ -1,4 +1,4 @@ -package tachiyomi.presentation.widget.components +package eu.kanade.tachiyomi.appwidget.components import android.content.Intent import android.graphics.Bitmap @@ -19,16 +19,18 @@ import androidx.glance.layout.padding import androidx.glance.text.Text import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.appwidget.ContainerModifier -import eu.kanade.tachiyomi.appwidget.components.UpdatesMangaCover import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount import eu.kanade.tachiyomi.appwidget.util.stringResource +import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity @Composable fun UpdatesWidget(data: List>?) { val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() + val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) Column( - modifier = ContainerModifier, + modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)), verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 7a0fe38e9ce0..cb4c1eb97bd9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -856,12 +856,13 @@ open class MainActivity : BaseActivity(), DownloadServiceLi } when (intent.action) { SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library - SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ -> { + SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, SHORTCUT_RECENTS -> { if (nav.selectedItemId != R.id.nav_recents) { nav.selectedItemId = R.id.nav_recents } else { router.popToRoot() } + if (intent.action == SHORTCUT_RECENTS) return false nav.post { val controller = router.backstack.firstOrNull()?.controller as? RecentsController @@ -1401,6 +1402,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi // Shortcut actions const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" + const val SHORTCUT_RECENTS = "eu.kanade.tachiyomi.SHOW_RECENTS" const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE" From f1768d391892b439b031d1d52ac909107558b7a5 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 15:22:32 -0500 Subject: [PATCH 11/61] random cleanup --- .../eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt | 1 - .../java/eu/kanade/tachiyomi/data/download/model/Download.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index 767e6df96b94..23de4c822377 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -34,7 +34,6 @@ interface ChapterQueries : DbProvider { .withQuery( RawQuery.builder() .query(getRecentsQuery(search.sqLite, offset, isResuming)) -// .args(date.time, startDate.time) .observesTables(ChapterTable.TABLE) .build(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 2b33fbdd01c3..b9886e092d06 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -56,7 +56,6 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { DOWNLOADING, DOWNLOADED, ERROR, - ; companion object { From 90054bf9d23ce9b6e53f876039b7bc22f2236f6a Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 15:22:43 -0500 Subject: [PATCH 12/61] update download button's ripple --- app/src/main/res/layout/download_button.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/download_button.xml b/app/src/main/res/layout/download_button.xml index c073dcba3ac4..a93780ddf23d 100644 --- a/app/src/main/res/layout/download_button.xml +++ b/app/src/main/res/layout/download_button.xml @@ -46,7 +46,7 @@ android:layout_width="30dp" android:contentDescription="@string/download" android:layout_height="30dp" - android:background="@drawable/round_ripple" + android:background="?selectableItemBackgroundBorderless" android:padding="5dp" android:src="@drawable/ic_arrow_downward_24dp" /> \ No newline at end of file From c4901ce732089f425b91daade9fb1e008493e471 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 15:40:03 -0500 Subject: [PATCH 13/61] helper method for hiding chapter titles also fix invalid chapter numbers showing "Chapter -1" --- .../ui/manga/chapter/ChapterHolder.kt | 9 +++------ .../tachiyomi/ui/reader/ReaderActivity.kt | 11 +++++------ .../ui/reader/chapter/ReaderChapterItem.kt | 14 +++----------- .../tachiyomi/ui/recents/RecentMangaHolder.kt | 12 +++++------- .../tachiyomi/util/chapter/ChapterUtil.kt | 19 +++++++++++++++++++ 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index e60861fe748b..58d5db17459d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.databinding.ChaptersItemBinding import eu.kanade.tachiyomi.ui.manga.MangaDetailsAdapter import eu.kanade.tachiyomi.util.chapter.ChapterUtil +import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.getResourceColor @@ -37,12 +38,8 @@ class ChapterHolder( val chapter = item.chapter val isLocked = item.isLocked itemView.transitionName = "details chapter ${chapter.id ?: 0L} transition" - binding.chapterTitle.text = if (manga.hideChapterTitle(adapter.preferences)) { - val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.chapter_, number) - } else { - chapter.name - } + binding.chapterTitle.text = + chapter.preferredChapterName(itemView.context, manga, adapter.preferences) binding.downloadButton.downloadButton.isVisible = !manga.isLocal() && !isLocked localSource = manga.isLocal() 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 6d516b9ec3e6..12487f41f1b2 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 @@ -97,6 +97,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.contextCompatColor import eu.kanade.tachiyomi.util.system.contextCompatDrawable @@ -1315,12 +1316,10 @@ class ReaderActivity : BaseActivity() { viewer?.setChapters(viewerChapters) intentPageNumber?.let { moveToPageIndex(it) } intentPageNumber = null - binding.toolbar.subtitle = if (viewModel.manga!!.hideChapterTitle(preferences)) { - val number = decimalFormat.format(viewerChapters.currChapter.chapter.chapter_number.toDouble()) - getString(R.string.chapter_, number) - } else { - viewerChapters.currChapter.chapter.name - } + val chapter = viewerChapters.currChapter.chapter + binding.toolbar.subtitle = + chapter.preferredChapterName(this, viewModel.manga!!, preferences) + if (viewerChapters.nextChapter == null && viewerChapters.prevChapter == null) { binding.readerNav.leftChapter.isVisible = false binding.readerNav.rightChapter.isVisible = false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/chapter/ReaderChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/chapter/ReaderChapterItem.kt index c9bf53cae853..e7874a448730 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/chapter/ReaderChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/chapter/ReaderChapterItem.kt @@ -11,17 +11,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.ReaderChapterItemBinding import eu.kanade.tachiyomi.util.chapter.ChapterUtil +import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName import uy.kohesive.injekt.injectLazy -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols class ReaderChapterItem(val chapter: Chapter, val manga: Manga, val isCurrent: Boolean) : AbstractItem(), Chapter by chapter { - val decimalFormat = - DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) - val preferences: PreferencesHelper by injectLazy() /** defines the type defining this item. must be unique. preferably an id */ @@ -44,12 +40,8 @@ class ReaderChapterItem(val chapter: Chapter, val manga: Manga, val isCurrent: B val chapterColor = ChapterUtil.chapterColor(itemView.context, item.chapter) - binding.chapterTitle.text = if (manga.hideChapterTitle(item.preferences)) { - val number = item.decimalFormat.format(item.chapter_number.toDouble()) - itemView.context.getString(R.string.chapter_, number) - } else { - item.name - } + binding.chapterTitle.text = + item.preferredChapterName(itemView.context, manga, item.preferences) val statuses = mutableListOf() ChapterUtil.relativeDate(item)?.let { statuses.add(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt index 84647fc315f6..5504e9e3f340 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.databinding.RecentMangaItemBinding import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.util.chapter.ChapterUtil +import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.timeSpanFromNow @@ -91,12 +92,10 @@ class RecentMangaHolder( } binding.removeHistory.isVisible = item.mch.history.id != null && showRemoveHistory - val chapterName = if (item.mch.manga.hideChapterTitle(adapter.preferences)) { - val number = adapter.decimalFormat.format(item.chapter.chapter_number.toDouble()) - itemView.context.getString(R.string.chapter_, number) - } else { - item.chapter.name - } + val context = itemView.context + val chapterName = + item.chapter.preferredChapterName(context, item.mch.manga, adapter.preferences) + binding.title.apply { text = if (!showTitleFirst) { chapterName @@ -120,7 +119,6 @@ class RecentMangaHolder( } val notValidNum = item.mch.chapter.chapter_number <= 0 binding.body.isVisible = !isSmallUpdates - val context = itemView.context binding.body.text = when { item.mch.chapter.id == null -> context.timeSpanFromNow(R.string.added_, item.mch.manga.date_added) isSmallUpdates -> "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt index b8f98ff35d1d..49de8c28a85f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt @@ -7,16 +7,26 @@ import androidx.core.widget.TextViewCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.util.system.contextCompatColor import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPxEnd import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.timeSpanFromNow +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols class ChapterUtil { companion object { + private val decimalFormat = DecimalFormat( + "#.###", + DecimalFormatSymbols() + .apply { decimalSeparator = '.' }, + ) + fun relativeDate(chapter: Chapter): String? { return when (chapter.date_upload > 0) { true -> chapter.date_upload.timeSpanFromNow @@ -153,5 +163,14 @@ class ChapterUtil { fun getScanlatorString(scanlators: Set): String { return scanlators.toList().sorted().joinToString(scanlatorSeparator) } + + fun Chapter.preferredChapterName(context: Context, manga: Manga, preferences: PreferencesHelper): String { + return if (manga.hideChapterTitle(preferences) && isRecognizedNumber) { + val number = decimalFormat.format(chapter_number.toDouble()) + context.getString(R.string.chapter_, number) + } else { + name + } + } } } From 3a1855bc1da2bfaf45d4530d31190adc3996efae Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 16:15:49 -0500 Subject: [PATCH 14/61] Updates to update section in recents Fetched header now updated to be a bit more modern; no more shadow Manga with multiple updates in a day are now grouped together, with the first unread chapter at the top (or last read chapter first) Few fixes to the ghost view behind the recycler (might adapt to other places soon enough) Group chapters together setting now changed to collapse grouped chapters, by default the lists are expanded This change also means that when grouping, manga will show again on a different day when previously it did not --- .../database/models/MangaChapterHistory.kt | 5 +- .../data/preference/PreferenceKeys.kt | 1 - .../data/preference/PreferencesHelper.kt | 2 +- .../ui/manga/chapter/BaseChapterAdapter.kt | 6 + .../ui/manga/chapter/BaseChapterHolder.kt | 38 +++- .../kanade/tachiyomi/ui/recents/DateItem.kt | 3 + .../ui/recents/RecentMangaAdapter.kt | 21 +- .../tachiyomi/ui/recents/RecentMangaHolder.kt | 189 ++++++++++++++++-- .../tachiyomi/ui/recents/RecentMangaItem.kt | 25 +++ .../tachiyomi/ui/recents/RecentsController.kt | 101 ++++++++-- .../tachiyomi/ui/recents/RecentsPresenter.kt | 117 ++++++++--- .../ui/recents/options/RecentsUpdatesView.kt | 4 +- .../layout/recent_chapters_section_item.xml | 14 +- app/src/main/res/layout/recent_manga_item.xml | 39 +++- .../res/layout/recent_sub_chapter_item.xml | 48 +++++ .../main/res/layout/recents_updates_view.xml | 2 +- app/src/main/res/values/strings.xml | 2 + 17 files changed, 524 insertions(+), 93 deletions(-) create mode 100644 app/src/main/res/layout/recent_sub_chapter_item.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index 224f6c53dbcc..cf8f5621e646 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -7,7 +7,10 @@ package eu.kanade.tachiyomi.data.database.models * @param chapter object containing chater * @param history object containing history */ -data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) { +data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, val extraChapters: List = emptyList()) { + + val allChapters: List + get() = listOf(chapter) + extraChapters companion object { fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index e787ccbdcf74..d80bb32a6b92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -197,7 +197,6 @@ object PreferenceKeys { const val groupChaptersHistory = "group_chapters_history" const val showUpdatedTime = "show_updated_time" - const val groupChaptersUpdates = "group_chapters_updates" const val categoryNumberOfItems = "display_number_of_items" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 0cdbaf4b9f6b..0c28aabad664 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -373,7 +373,7 @@ class PreferencesHelper(val context: Context) { fun sortFetchedTime() = flowPrefs.getBoolean("sort_fetched_time", false) - fun groupChaptersUpdates() = flowPrefs.getBoolean(Keys.groupChaptersUpdates, false) + fun collapseGroupedUpdates() = flowPrefs.getBoolean("group_chapters_updates", false) fun groupChaptersHistory() = flowPrefs.getBoolean(Keys.groupChaptersHistory, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt index a41d3364554a..3472a7aa18c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterAdapter.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.data.database.models.Chapter open class BaseChapterAdapter>( obj: DownloadInterface, @@ -13,4 +14,9 @@ open class BaseChapterAdapter>( fun downloadChapter(position: Int) fun startDownloadNow(position: Int) } + + interface GroupedDownloadInterface : DownloadInterface { + fun downloadChapter(position: Int, chapter: Chapter) + fun startDownloadNow(position: Int, chapter: Chapter) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt index 5c651056b1e7..c7db39c0eb55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/BaseChapterHolder.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter import android.view.View import androidx.appcompat.widget.PopupMenu import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder @@ -12,15 +13,20 @@ open class BaseChapterHolder( ) : BaseFlexibleViewHolder(view, adapter) { init { - view.findViewById(R.id.download_button)?.setOnClickListener { downloadOrRemoveMenu() } + view.findViewById(R.id.download_button)?.setOnClickListener { downloadOrRemoveMenu(it) } } - private fun downloadOrRemoveMenu() { + internal fun downloadOrRemoveMenu(downloadButton: View, extraChapter: Chapter? = null, extraStatus: Download.State? = null) { val chapter = adapter.getItem(flexibleAdapterPosition) as? BaseChapterItem<*, *> ?: return - val downloadButton = itemView.findViewById(R.id.download_button) ?: return - if (chapter.status == Download.State.NOT_DOWNLOADED || chapter.status == Download.State.ERROR) { - adapter.baseDelegate.downloadChapter(flexibleAdapterPosition) + val chapterStatus = extraStatus ?: chapter.status + if (chapterStatus == Download.State.NOT_DOWNLOADED || chapterStatus == Download.State.ERROR) { + if (extraChapter != null) { + (adapter.baseDelegate as? BaseChapterAdapter.GroupedDownloadInterface) + ?.downloadChapter(flexibleAdapterPosition, extraChapter) + } else { + adapter.baseDelegate.downloadChapter(flexibleAdapterPosition) + } } else { downloadButton.post { // Create a PopupMenu, giving it the clicked view for an anchor @@ -29,10 +35,10 @@ open class BaseChapterHolder( // Inflate our menu resource into the PopupMenu's Menu popup.menuInflater.inflate(R.menu.chapter_download, popup.menu) - popup.menu.findItem(R.id.action_start).isVisible = chapter.status == Download.State.QUEUE + popup.menu.findItem(R.id.action_start).isVisible = chapterStatus == Download.State.QUEUE // Hide download and show delete if the chapter is downloaded - if (chapter.status != Download.State.DOWNLOADED) { + if (chapterStatus != Download.State.DOWNLOADED) { popup.menu.findItem(R.id.action_delete).title = downloadButton.context.getString( R.string.cancel, ) @@ -41,8 +47,22 @@ open class BaseChapterHolder( // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { item -> when (item.itemId) { - R.id.action_delete -> adapter.baseDelegate.downloadChapter(flexibleAdapterPosition) - R.id.action_start -> adapter.baseDelegate.startDownloadNow(flexibleAdapterPosition) + R.id.action_delete -> { + if (extraChapter != null) { + (adapter.baseDelegate as? BaseChapterAdapter.GroupedDownloadInterface) + ?.downloadChapter(flexibleAdapterPosition, extraChapter) + } else { + adapter.baseDelegate.downloadChapter(flexibleAdapterPosition) + } + } + R.id.action_start -> { + if (extraChapter != null) { + (adapter.baseDelegate as? BaseChapterAdapter.GroupedDownloadInterface) + ?.startDownloadNow(flexibleAdapterPosition, extraChapter) + } else { + adapter.baseDelegate.startDownloadNow(flexibleAdapterPosition) + } + } } true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/DateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/DateItem.kt index d5419149a40c..14d8a9b77c8c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/DateItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/DateItem.kt @@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.ui.recents import android.text.format.DateUtils import android.view.View import android.widget.TextView +import androidx.core.view.updatePadding import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.dpToPx import java.util.Date class DateItem(val date: Date, val addedString: Boolean = false) : AbstractHeaderItem() { @@ -48,6 +50,7 @@ class DateItem(val date: Date, val addedString: Boolean = false) : AbstractHeade private val sectionText: TextView = view.findViewById(R.id.section_text) fun bind(item: DateItem) { + sectionText.updatePadding(top = (if (bindingAdapterPosition == 0) 4 else 18).dpToPx) val dateString = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) sectionText.text = if (item.addedString) itemView.context.getString(R.string.fetched_, dateString) else dateString diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt index 0d5e9a9d7f14..d0cfd596053c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt @@ -1,8 +1,10 @@ package eu.kanade.tachiyomi.ui.recents +import android.view.View import androidx.recyclerview.widget.ItemTouchHelper import com.fredporciuncula.flow.preferences.Preference import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter @@ -30,10 +32,6 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : val viewType: Int get() = delegate.getViewType() - fun updateItems(items: List>?) { - updateDataSet(items) - } - val decimalFormat = DecimalFormat( "#.###", DecimalFormatSymbols() @@ -50,6 +48,7 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : preferences.showTitleFirstInRecents().register { showTitleFirst = it } preferences.showUpdatedTime().register { showUpdatedTime = it } preferences.uniformGrid().register { uniformCovers = it } + preferences.collapseGroupedUpdates().register { } preferences.sortFetchedTime().asImmediateFlowIn(delegate.scope()) { sortByFetched = it } preferences.outlineOnCovers().register(false) { showOutline = it @@ -59,6 +58,13 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : } } + fun getItemByChapterId(id: Long): RecentMangaItem? { + return currentItems.find { + val item = (it as? RecentMangaItem) ?: return@find false + return@find id in item.mch.allChapters.map { ch -> ch.id } + } as? RecentMangaItem + } + private fun Preference.register(notify: Boolean = true, onChanged: (T) -> Unit) { asFlow() .drop(1) @@ -71,11 +77,12 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : .launchIn(delegate.scope()) } - interface RecentsInterface : RecentMangaInterface, DownloadInterface - - interface RecentMangaInterface { + interface RecentsInterface : GroupedDownloadInterface { fun onCoverClick(position: Int) fun onRemoveHistoryClicked(position: Int) + fun onSubChapterClicked(position: Int, chapter: Chapter, view: View) + fun updateExpandedExtraChapters(position: Int, expanded: Boolean) + fun areExtraChaptersExpanded(position: Int): Boolean fun markAsRead(position: Int) fun isSearching(): Boolean fun scope(): CoroutineScope diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt index 5504e9e3f340..cff36bc65fcf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt @@ -1,22 +1,35 @@ package eu.kanade.tachiyomi.ui.recents +import android.animation.LayoutTransition +import android.annotation.SuppressLint import android.app.Activity +import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.children +import androidx.core.view.forEach import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePaddingRelative +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.databinding.RecentMangaItemBinding +import eu.kanade.tachiyomi.databinding.RecentSubChapterItemBinding +import eu.kanade.tachiyomi.ui.download.DownloadButton import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.timeSpanFromNow +import eu.kanade.tachiyomi.util.view.setAnimVectorCompat import eu.kanade.tachiyomi.util.view.setCards class RecentMangaHolder( @@ -25,20 +38,55 @@ class RecentMangaHolder( ) : BaseChapterHolder(view, adapter) { private val binding = RecentMangaItemBinding.bind(view) + var chapterId: Long? = null + private val isSmallUpdates + get() = adapter.viewType == RecentsPresenter.VIEW_TYPE_ONLY_UPDATES && + !adapter.showUpdatedTime init { binding.cardLayout.setOnClickListener { adapter.delegate.onCoverClick(flexibleAdapterPosition) } binding.removeHistory.setOnClickListener { adapter.delegate.onRemoveHistoryClicked(flexibleAdapterPosition) } + binding.showMoreChapters.setOnClickListener { + val moreVisible = !binding.moreChaptersLayout.isVisible + binding.moreChaptersLayout.isVisible = moreVisible + adapter.delegate.updateExpandedExtraChapters(flexibleAdapterPosition, moreVisible) + binding.showMoreChapters.setAnimVectorCompat( + if (moreVisible) { + R.drawable.anim_expand_more_to_less + } else { + R.drawable.anim_expand_less_to_more + }, + ) + if (moreVisible) { + binding.moreChaptersLayout.children.forEach { view -> + try { + RecentSubChapterItemBinding.bind(view).updateDivider() + } catch (_: Exception) { + } + } + } + binding.endView.updateLayoutParams { + height = binding.mainView.height + } + val transition = TransitionSet() + .addTransition(androidx.transition.ChangeBounds()) + .addTransition(androidx.transition.Slide()) + transition.duration = it.resources.getInteger(android.R.integer.config_shortAnimTime) + .toLong() + TransitionManager.beginDelayedTransition(adapter.recyclerView, transition) + } updateCards() + binding.frontView.layoutTransition?.enableTransitionType(LayoutTransition.APPEARING) } fun updateCards() { setCards(adapter.showOutline, binding.card, null) } + @SuppressLint("ClickableViewAccessibility") fun bind(item: RecentMangaItem) { val showDLs = adapter.showDownloads - itemView.transitionName = "recents chapter $bindingAdapterPosition transition" + binding.mainView.transitionName = "recents chapter $bindingAdapterPosition transition" val showRemoveHistory = adapter.showRemoveHistory val showTitleFirst = adapter.showTitleFirst binding.downloadButton.downloadButton.isVisible = when (showDLs) { @@ -48,8 +96,6 @@ class RecentMangaHolder( RecentMangaAdapter.ShowRecentsDLs.All -> true } && !item.mch.manga.isLocal() - val isSmallUpdates = adapter.viewType == RecentsPresenter.VIEW_TYPE_ONLY_UPDATES && - !adapter.showUpdatedTime binding.cardLayout.updateLayoutParams { height = (if (isSmallUpdates) 40 else 80).dpToPx width = (if (isSmallUpdates) 40 else 60).dpToPx @@ -63,14 +109,14 @@ class RecentMangaHolder( } else { if (it == binding.title) topMargin = 2.dpToPx endToStart = -1 - endToEnd = R.id.front_view + endToEnd = R.id.main_view } } } binding.buttonLayout.updateLayoutParams { if (isSmallUpdates) { topToBottom = -1 - topToTop = R.id.front_view + topToTop = R.id.main_view } else { topToTop = -1 topToBottom = R.id.subtitle @@ -157,16 +203,117 @@ class RecentMangaHolder( item.chapter.read, ) } - resetFrontView() + + binding.showMoreChapters.isVisible = item.mch.extraChapters.isNotEmpty() + binding.moreChaptersLayout.isVisible = binding.showMoreChapters.isVisible && + adapter.delegate.areExtraChaptersExpanded(flexibleAdapterPosition) + val moreVisible = binding.moreChaptersLayout.isVisible + binding.showMoreChapters.setImageResource( + if (moreVisible) { + R.drawable.ic_expand_less_24dp + } else { + R.drawable.ic_expand_more_24dp + }, + ) + val extraIds = binding.moreChaptersLayout.children.map { + it.findViewById(R.id.download_button)?.tag + }.toList() + if (extraIds == item.mch.extraChapters.map { it.id }) { + item.mch.extraChapters.forEachIndexed { index, chapter -> + RecentSubChapterItemBinding.bind(binding.moreChaptersLayout.getChildAt(index)) + .configureView(chapter, item) + } + } else { + binding.moreChaptersLayout.removeAllViews() + if (item.mch.extraChapters.isNotEmpty()) { + item.mch.extraChapters.forEach { chapter -> + RecentSubChapterItemBinding.inflate( + LayoutInflater.from(context), + binding.moreChaptersLayout, + true, + ).configureView(chapter, item) + } + } else { + chapterId = null + } + } + listOf(binding.mainView, binding.downloadButton.root, binding.showMoreChapters, binding.cardLayout).forEach { + it.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + binding.endView.translationY = binding.mainView.y + binding.endView.updateLayoutParams { + height = binding.mainView.height + } + binding.read.setImageResource( + if (item.read) R.drawable.ic_eye_off_24dp else R.drawable.ic_eye_24dp, + ) + chapterId = null + } + false + } + } } - private fun resetFrontView() { - if (binding.frontView.translationX != 0f) { - itemView.post { - androidx.transition.TransitionManager.endTransitions(adapter.recyclerView) - adapter.notifyItemChanged(flexibleAdapterPosition) + @SuppressLint("ClickableViewAccessibility") + private fun RecentSubChapterItemBinding.configureView(chapter: Chapter, item: RecentMangaItem) { + val context = itemView.context + val showDLs = adapter.showDownloads + title.text = chapter.preferredChapterName(context, item.mch.manga, adapter.preferences) + title.setTextColor(ChapterUtil.readColor(context, chapter)) + root.setOnClickListener { + adapter.delegate.onSubChapterClicked( + flexibleAdapterPosition, + chapter, + it, + ) + } + listOf(root, downloadButton.root).forEach { + it.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + binding.read.setImageResource( + if (chapter.read) R.drawable.ic_eye_off_24dp else R.drawable.ic_eye_24dp, + ) + binding.endView.translationY = binding.moreChaptersLayout.y + root.y + binding.endView.updateLayoutParams { + height = root.height + } + chapterId = chapter.id + } + false } } + title.updatePaddingRelative(start = if (isSmallUpdates) 64.dpToPx else 84.dpToPx) + updateDivider() + root.transitionName = "recents sub chapter ${chapter.id ?: 0L} transition" + root.tag = "sub ${chapter.id}" + downloadButton.root.tag = chapter.id + val downloadInfo = + item.downloadInfo.find { it.chapterId == chapter.id } ?: return + downloadButton.downloadButton.setOnClickListener { + downloadOrRemoveMenu(it, chapter, downloadInfo.status) + } + downloadButton.downloadButton.isVisible = when (showDLs) { + RecentMangaAdapter.ShowRecentsDLs.None -> false + RecentMangaAdapter.ShowRecentsDLs.OnlyUnread, RecentMangaAdapter.ShowRecentsDLs.UnreadOrDownloaded -> !chapter.read + RecentMangaAdapter.ShowRecentsDLs.OnlyDownloaded -> true + RecentMangaAdapter.ShowRecentsDLs.All -> true + } && !item.mch.manga.isLocal() + notifySubStatus( + chapter, + if (adapter.isSelected(flexibleAdapterPosition)) { + Download.State.CHECKED + } else { + downloadInfo.status + }, + downloadInfo.progress, + chapter.read, + ) + } + + private fun RecentSubChapterItemBinding.updateDivider() { + divider.updateLayoutParams { + marginStart = if (isSmallUpdates) 64.dpToPx else 84.dpToPx + } } override fun onLongClick(view: View?): Boolean { @@ -189,8 +336,26 @@ class RecentMangaHolder( } } + fun notifySubStatus(chapter: Chapter, status: Download.State, progress: Int, isRead: Boolean, animated: Boolean = false) { + val downloadButton = binding.moreChaptersLayout.findViewWithTag(chapter.id) ?: return + downloadButton.setDownloadStatus(status, progress, animated) + val isChapterRead = + if (adapter.showDownloads == RecentMangaAdapter.ShowRecentsDLs.UnreadOrDownloaded) isRead else true + downloadButton.isVisible = + when (adapter.showDownloads) { + RecentMangaAdapter.ShowRecentsDLs.UnreadOrDownloaded, + RecentMangaAdapter.ShowRecentsDLs.OnlyDownloaded, + -> + status !in Download.State.CHECKED..Download.State.NOT_DOWNLOADED || !isChapterRead + else -> downloadButton.isVisible + } + } + override fun getFrontView(): View { - return binding.frontView + return if (chapterId == null) { binding.mainView } else { + binding.moreChaptersLayout.children.find { it.tag == "sub $chapterId" } + ?: binding.mainView + } } override fun getRearEndView(): View { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt index f10e15236784..054a0edd01fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaItem.kt @@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterItem @@ -19,6 +21,8 @@ class RecentMangaItem( ) : BaseChapterItem>(chapter, header) { + var downloadInfo = listOf() + override fun getLayoutRes(): Int { return if (mch.manga.id == null) { R.layout.recents_footer_item @@ -73,4 +77,25 @@ class RecentMangaItem( (holder as? RecentMangaFooterHolder)?.bind((header as? RecentMangaHeaderItem)?.recentsType ?: 0) } else if (chapter.id != null) (holder as? RecentMangaHolder)?.bind(this) } + + class DownloadInfo { + private var _status: Download.State = Download.State.default + + var chapterId: Long? = 0L + + val progress: Int + get() { + val pages = download?.pages ?: return 0 + return pages.map(Page::progress).average().toInt() + } + + var status: Download.State + get() = download?.status ?: _status + set(value) { _status = value } + + @Transient var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.State.DOWNLOADED + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 8645b9ccad07..37e33976636a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -20,6 +20,7 @@ import androidx.core.view.updatePaddingRelative import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionSet import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -29,6 +30,7 @@ import com.google.android.material.tabs.TabLayout import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupRestoreService +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadService @@ -163,9 +165,7 @@ class RecentsController(bundle: Bundle? = null) : binding.recycler.layoutManager = LinearLayoutManagerAccurateOffset(view.context) binding.recycler.setHasFixedSize(true) binding.recycler.recycledViewPool.setMaxRecycledViews(0, 0) - binding.recycler.addItemDecoration( - RecentMangaDivider(view.context), - ) + binding.recycler.addItemDecoration(RecentMangaDivider(view.context)) adapter.isSwipeEnabled = true adapter.itemTouchHelperCallback.setSwipeFlags( if (view.resources.isLTR) ItemTouchHelper.LEFT else ItemTouchHelper.RIGHT, @@ -513,7 +513,7 @@ class RecentsController(bundle: Bundle? = null) : binding.frameLayout.alpha = 1f binding.swipeRefresh.isRefreshing = LibraryUpdateService.isRunning() adapter.removeAllScrollableHeaders() - adapter.updateItems(recents) + adapter.updateDataSet(recents) adapter.onLoadMoreComplete(null) if (isControllerVisible) { activityBinding?.appBar?.lockYPos = false @@ -568,8 +568,19 @@ class RecentsController(bundle: Bundle? = null) : binding.downloadBottomSheet.dlBottomSheet.onUpdateDownloadedPages(download) } val id = download.chapter.id ?: return - val holder = binding.recycler.findViewHolderForItemId(id) as? RecentMangaHolder ?: return - holder.notifyStatus(download.status, download.progress, download.chapter.read, true) + val item = adapter.getItemByChapterId(id) ?: return + val holder = binding.recycler.findViewHolderForItemId(item.id!!) as? RecentMangaHolder ?: return + if (item.id == id) { + holder.notifyStatus(download.status, download.progress, download.chapter.read, true) + } else { + holder.notifySubStatus( + download.chapter, + download.status, + download.progress, + download.chapter.read, + true, + ) + } } fun updateDownloadStatus(isRunning: Boolean) { @@ -605,6 +616,26 @@ class RecentsController(bundle: Bundle? = null) : presenter.startDownloadChapterNow(chapter) } + override fun downloadChapter(position: Int, chapter: Chapter) { + val view = view ?: return + val item = adapter.getItem(position) as? RecentMangaItem ?: return + val manga = item.mch.manga + val status = item.downloadInfo.find { it.chapterId == chapter.id }?.status ?: return + if (status != Download.State.NOT_DOWNLOADED && status != Download.State.ERROR) { + presenter.deleteChapter(chapter, manga) + } else { + if (status == Download.State.ERROR) { + DownloadService.start(view.context) + } else { + presenter.downloadChapter(manga, chapter) + } + } + } + + override fun startDownloadNow(position: Int, chapter: Chapter) { + presenter.startDownloadChapterNow(chapter) + } + override fun onCoverClick(position: Int) { val manga = (adapter.getItem(position) as? RecentMangaItem)?.mch?.manga ?: return router.pushController(MangaDetailsController(manga).withFadeTransaction()) @@ -614,6 +645,25 @@ class RecentsController(bundle: Bundle? = null) : onItemLongClick(position) } + override fun onSubChapterClicked(position: Int, chapter: Chapter, view: View) { + val manga = (adapter.getItem(position) as? RecentMangaItem)?.mch?.manga ?: return + openChapter(view, manga, chapter) + } + + override fun areExtraChaptersExpanded(position: Int): Boolean { + val item = (adapter.getItem(position) as? RecentMangaItem) ?: return false + val date = presenter.dateFormat.format(item.chapter.date_fetch) + val invertDefault = !presenter.preferences.collapseGroupedUpdates().get() + return presenter.expandedSectionsMap["${item.mch.manga} - $date"]?.xor(invertDefault) + ?: invertDefault + } + + override fun updateExpandedExtraChapters(position: Int, expanded: Boolean) { + val item = (adapter.getItem(position) as? RecentMangaItem) ?: return + val date = presenter.dateFormat.format(item.chapter.date_fetch) + presenter.expandedSectionsMap["${item.mch.manga} - $date"] = expanded + } + fun tempJumpTo(viewType: Int) { presenter.toggleGroupRecents(viewType, false) activityBinding?.mainTabs?.selectTab(activityBinding?.mainTabs?.getTabAt(viewType)) @@ -645,23 +695,27 @@ class RecentsController(bundle: Bundle? = null) : }, ) } else { - val activity = activity ?: return false - activity.apply { - val (manga, chapter) = item.mch.manga to item.chapter - if (view != null) { - val (intent, bundle) = ReaderActivity - .newIntentWithTransitionOptions(activity, manga, chapter, view) - startActivity(intent, bundle) - } else { - val intent = ReaderActivity.newIntent(activity, manga, chapter) - startActivity(intent) - } - } + if (activity == null) return false + openChapter(view?.findViewById(R.id.main_view), item.mch.manga, item.chapter) } } else if (item is RecentMangaHeaderItem) return false return true } + private fun openChapter(view: View?, manga: Manga, chapter: Chapter) { + val activity = activity ?: return + activity.apply { + if (view != null) { + val (intent, bundle) = ReaderActivity + .newIntentWithTransitionOptions(activity, manga, chapter, view) + startActivity(intent, bundle) + } else { + val intent = ReaderActivity.newIntent(activity, manga, chapter) + startActivity(intent) + } + } + } + override fun onItemLongClick(position: Int) { val item = adapter.getItem(position) as? RecentMangaItem ?: return val manga = item.mch.manga @@ -686,7 +740,15 @@ class RecentsController(bundle: Bundle? = null) : val preferences = presenter.preferences val db = presenter.db val item = adapter.getItem(position) as? RecentMangaItem ?: return - val chapter = item.chapter + val holder = binding.recycler.findViewHolderForAdapterPosition(position) + val holderId = (holder as? RecentMangaHolder)?.chapterId + adapter.notifyItemChanged(position) + val transition = TransitionSet().addTransition(androidx.transition.Fade()) + transition.duration = view!!.resources.getInteger(android.R.integer.config_shortAnimTime) + .toLong() + androidx.transition.TransitionManager.beginDelayedTransition(binding.recycler, transition) + val chapter = holderId?.let { item.mch.extraChapters.find { holderId == it.id } } + ?: item.chapter val manga = item.mch.manga val lastRead = chapter.last_page_read val pagesLeft = chapter.pages_left @@ -743,7 +805,6 @@ class RecentsController(bundle: Bundle? = null) : setOnQueryTextChangeListener(activityBinding?.searchToolbar?.searchView) { if (query != it) { query = it ?: return@setOnQueryTextChangeListener false - // loadNoMore() resetProgressItem() refresh() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 857378851502..3f557036ba7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.HistoryImpl import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService @@ -33,8 +34,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.Locale import java.util.TreeMap import java.util.concurrent.TimeUnit import kotlin.math.abs @@ -66,11 +69,14 @@ class RecentsPresenter( private var shouldMoveToTop = false var viewType: Int = preferences.recentsViewType().get() private set + val expandedSectionsMap = mutableMapOf() + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) private fun resetOffsets() { finished = false shouldMoveToTop = true pageOffset = 0 + expandedSectionsMap.clear() } private var pageOffset = 0 @@ -95,7 +101,6 @@ class RecentsPresenter( listOf( preferences.groupChaptersHistory(), preferences.showReadInAllRecents(), - preferences.groupChaptersUpdates(), preferences.sortFetchedTime(), ).forEach { it.asFlow() @@ -151,7 +156,7 @@ class RecentsPresenter( val showRead = ((preferences.showReadInAllRecents().get() || query.isNotEmpty()) && limit != 0) || includeReadAnyway val isUngrouped = viewType > VIEW_TYPE_GROUP_ALL || query.isNotEmpty() - val groupChaptersUpdates = preferences.groupChaptersUpdates().get() + val groupChaptersUpdates = preferences.collapseGroupedUpdates().get() val groupChaptersHistory = preferences.groupChaptersHistory().get() val isCustom = customViewType != null @@ -182,40 +187,54 @@ class RecentsPresenter( }.executeOnIO() } viewType == VIEW_TYPE_ONLY_UPDATES -> { - if (groupChaptersUpdates) { - db.getUpdatedChaptersDistinct( - query, - if (isCustom) ENDLESS_LIMIT else pageOffset, - !updatePageCount && !isOnFirstPage, - ) - } else { - db.getRecentChapters( - query, - if (isCustom) ENDLESS_LIMIT else pageOffset, - !updatePageCount && !isOnFirstPage, - ) - }.executeOnIO() - .map { + db.getRecentChapters( + query, + if (isCustom) ENDLESS_LIMIT else pageOffset, + !updatePageCount && !isOnFirstPage, + ).executeOnIO().groupBy { + val date = it.chapter.date_fetch + it.manga.id to if (date <= 0L) "-1" else dateFormat.format(Date(date)) + } + .map { entry -> + val manga = entry.value.first().manga + val chapters = entry.value.map(MangaChapter::chapter) + val firstChapter: Chapter + var sortedChapters: MutableList + if (chapters.size == 1) { + firstChapter = chapters.first() + sortedChapters = mutableListOf() + } else { + val sort: Comparator = + ChapterSort(manga, chapterFilter, preferences) + .sortComparator(true) + sortedChapters = chapters.sortedWith(sort).toMutableList() + firstChapter = + sortedChapters.firstOrNull { !it.read } ?: sortedChapters.last() + sortedChapters.remove(firstChapter) + sortedChapters = ( + sortedChapters.filter { !it.read } + + sortedChapters.filter { it.read }.reversed() + ).toMutableList() + } MangaChapterHistory( - it.manga, - it.chapter, - HistoryImpl().apply { - last_read = it.chapter.date_fetch - }, + manga, + firstChapter, + HistoryImpl().apply { last_read = firstChapter.date_fetch }, + sortedChapters, ) } } else -> emptyList() } - if (cReading.size < ENDLESS_LIMIT) { + if (cReading.size + cReading.sumOf { it.extraChapters.size } < ENDLESS_LIMIT) { finished = true } if (!isCustom && (pageOffset == 0 || updatePageCount) ) { - pageOffset += cReading.size + pageOffset += cReading.size + cReading.sumOf { it.extraChapters.size } } if (query != oldQuery) return @@ -242,9 +261,9 @@ class RecentsPresenter( getNextChapter(it.manga) ?: if (showRead && it.chapter.id != null) it.chapter else null } - it.history.id == null -> { + it.history.id == null && viewType != VIEW_TYPE_ONLY_UPDATES -> { getFirstUpdatedChapter(it.manga, it.chapter) - ?: if ((showRead && it.chapter.id != null) || viewType == VIEW_TYPE_ONLY_UPDATES) it.chapter else null + ?: if ((showRead && it.chapter.id != null)) it.chapter else null } else -> { it.chapter @@ -330,7 +349,7 @@ class RecentsPresenter( } else { heldItems[customViewType] = newItems } - val newCount = itemCount + newItems.size + val newCount = itemCount + newItems.size + newItems.sumOf { it.mch.extraChapters.size } val hasNewItems = newItems.isNotEmpty() if (updatePageCount && (newCount < if (limit > 0) limit else 25) && (viewType != VIEW_TYPE_GROUP_ALL || query.isNotEmpty()) && @@ -390,14 +409,34 @@ class RecentsPresenter( if (downloadManager.isChapterDownloaded(item.chapter, item.mch.manga)) { item.status = Download.State.DOWNLOADED } else if (downloadManager.hasQueue()) { - item.status = downloadManager.queue.find { it.chapter.id == item.chapter.id } - ?.status ?: Download.State.default + item.download = downloadManager.queue.find { it.chapter.id == item.chapter.id } + item.status = item.download?.status ?: Download.State.default + } + + item.downloadInfo = item.mch.extraChapters.map { chapter -> + val downloadInfo = RecentMangaItem.DownloadInfo() + downloadInfo.chapterId = chapter.id + if (downloadManager.isChapterDownloaded(chapter, item.mch.manga)) { + downloadInfo.status = Download.State.DOWNLOADED + } else if (downloadManager.hasQueue()) { + downloadInfo.download = downloadManager.queue.find { it.chapter.id == chapter.id } + downloadInfo.status = downloadInfo.download?.status ?: Download.State.default + } + downloadInfo } } } override fun updateDownload(download: Download) { - recentItems.find { it.chapter.id == download.chapter.id }?.download = download + recentItems.find { download.chapter.id in it.mch.allChapters.map { ch -> ch.id } }?.apply { + if (chapter.id != download.chapter.id) { + val downloadInfo = downloadInfo.find { it.chapterId == download.chapter.id } + ?: return@apply + downloadInfo.download = download + } else { + this.download = download + } + } presenterScope.launchUI { controller?.updateChapterDownload(download) } } @@ -443,11 +482,23 @@ class RecentsPresenter( downloadManager.deleteChapters(listOf(chapter), manga, source, true) } if (update) { - val item = recentItems.find { it.chapter.id == chapter.id } ?: return + val item = recentItems.find { chapter.id in it.mch.allChapters.map { ch -> ch.id } } ?: return item.apply { - if (chapter.bookmark && !preferences.removeBookmarkedChapters().get()) return@apply - status = Download.State.NOT_DOWNLOADED - download = null + if (chapter.id != item.chapter.id) { + val extraChapter = mch.extraChapters.find { it.id == chapter.id } ?: return@apply + val downloadInfo = downloadInfo.find { it.chapterId == chapter.id } ?: return@apply + if (extraChapter.bookmark && !preferences.removeBookmarkedChapters().get()) { + return@apply + } + downloadInfo.status = Download.State.NOT_DOWNLOADED + downloadInfo.download = null + } else { + if (chapter.bookmark && !preferences.removeBookmarkedChapters().get()) { + return@apply + } + status = Download.State.NOT_DOWNLOADED + download = null + } } controller?.showLists(recentItems, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/options/RecentsUpdatesView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/options/RecentsUpdatesView.kt index 6f273cb83fec..adda8d5dac68 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/options/RecentsUpdatesView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/options/RecentsUpdatesView.kt @@ -13,6 +13,8 @@ class RecentsUpdatesView @JvmOverloads constructor(context: Context, attrs: Attr override fun initGeneralPreferences() { binding.showUpdatedTime.bindToPreference(preferences.showUpdatedTime()) binding.sortFetchedTime.bindToPreference(preferences.sortFetchedTime()) - binding.groupChapters.bindToPreference(preferences.groupChaptersUpdates()) + binding.groupChapters.bindToPreference(preferences.collapseGroupedUpdates()) { + controller?.presenter?.expandedSectionsMap?.clear() + } } } diff --git a/app/src/main/res/layout/recent_chapters_section_item.xml b/app/src/main/res/layout/recent_chapters_section_item.xml index 6cb090481bf3..efaf58e56cf4 100644 --- a/app/src/main/res/layout/recent_chapters_section_item.xml +++ b/app/src/main/res/layout/recent_chapters_section_item.xml @@ -1,17 +1,21 @@ + app:cardCornerRadius="0dp"> - + + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recent_sub_chapter_item.xml b/app/src/main/res/layout/recent_sub_chapter_item.xml new file mode 100644 index 000000000000..0b16d52892a7 --- /dev/null +++ b/app/src/main/res/layout/recent_sub_chapter_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recents_updates_view.xml b/app/src/main/res/layout/recents_updates_view.xml index f81d6d734027..fbad31e3ac3d 100644 --- a/app/src/main/res/layout/recents_updates_view.xml +++ b/app/src/main/res/layout/recents_updates_view.xml @@ -38,7 +38,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="@string/group_chapters_together" + android:text="@string/collapse_grouped_chapters" android:textColor="?attr/colorOnBackground" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e90c67617f0..6a1118981e1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -259,6 +259,8 @@ Fetched %1$s View history View all updates + Show more chapters + Collapse grouped chapters Search recents… No recently read or updated manga No recent chapters From 6182ae73d7b5395803a0ebd453049d7596e98179 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 16:44:50 -0500 Subject: [PATCH 15/61] cleanup unused methods in last commit --- .../data/database/queries/ChapterQueries.kt | 17 -------------- .../data/database/queries/RawQueries.kt | 23 ------------------- .../ui/recents/RecentMangaAdapter.kt | 3 ++- .../tachiyomi/ui/recents/RecentsController.kt | 2 +- 4 files changed, 3 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index 23de4c822377..e25d5588841a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -40,23 +40,6 @@ interface ChapterQueries : DbProvider { .withGetResolver(MangaChapterGetResolver.INSTANCE) .prepare() - /** - * Returns history of recent manga containing last read chapter in 25s - * @param date recent date range - * @offset offset the db by - */ - fun getUpdatedChaptersDistinct(search: String = "", offset: Int, isResuming: Boolean) = db.get() - .listOfObjects(MangaChapter::class.java) - .withQuery( - RawQuery.builder() - .query(getRecentsQueryDistinct(search.sqLite, offset, isResuming)) -// .args(date.time, startDate.time) - .observesTables(ChapterTable.TABLE) - .build(), - ) - .withGetResolver(MangaChapterGetResolver.INSTANCE) - .prepare() - fun getChapter(id: Long) = db.get() .`object`(Chapter::class.java) .withQuery( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 3f7e876b55d4..322d32ce214c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -81,29 +81,6 @@ fun limitAndOffset(endless: Boolean, isResuming: Boolean, offset: Int): String { } } -/** - * Query to get the manga with recently uploaded chapters - */ -fun getRecentsQueryDistinct(search: String, offset: Int = 0, isResuming: Boolean) = - """ - SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.* - FROM ${Manga.TABLE} - JOIN ${Chapter.TABLE} - ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} - JOIN ( - SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID},MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) - FROM ${Chapter.TABLE} JOIN ${Manga.TABLE} - ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} - GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS newest_chapter - ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = newest_chapter.${Chapter.COL_MANGA_ID} - WHERE ${Manga.COL_FAVORITE} = 1 - AND newest_chapter.${Chapter.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} - AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED} - AND lower(${Manga.COL_TITLE}) LIKE '%$search%' - ORDER BY ${Chapter.COL_DATE_FETCH} DESC - ${limitAndOffset(true, isResuming, offset)} -""" - /** * Query to get the recently read chapters of manga from the library up to a date. * The max_last_read table contains the most recent chapters grouped by manga diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt index d0cfd596053c..dcc3077e5bd2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt @@ -28,6 +28,7 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : var uniformCovers = preferences.uniformGrid().get() var showOutline = preferences.outlineOnCovers().get() var sortByFetched = preferences.sortFetchedTime().get() + var collapseGroupedUpdates = preferences.collapseGroupedUpdates().get() val viewType: Int get() = delegate.getViewType() @@ -48,7 +49,7 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : preferences.showTitleFirstInRecents().register { showTitleFirst = it } preferences.showUpdatedTime().register { showUpdatedTime = it } preferences.uniformGrid().register { uniformCovers = it } - preferences.collapseGroupedUpdates().register { } + preferences.collapseGroupedUpdates().register { collapseGroupedUpdates = it } preferences.sortFetchedTime().asImmediateFlowIn(delegate.scope()) { sortByFetched = it } preferences.outlineOnCovers().register(false) { showOutline = it diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 37e33976636a..7cd946ce0d2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -653,7 +653,7 @@ class RecentsController(bundle: Bundle? = null) : override fun areExtraChaptersExpanded(position: Int): Boolean { val item = (adapter.getItem(position) as? RecentMangaItem) ?: return false val date = presenter.dateFormat.format(item.chapter.date_fetch) - val invertDefault = !presenter.preferences.collapseGroupedUpdates().get() + val invertDefault = !adapter.collapseGroupedUpdates return presenter.expandedSectionsMap["${item.mch.manga} - $date"]?.xor(invertDefault) ?: invertDefault } From ec30ad1930ccc66a58b88699e0dc8b6cdf6a2ccf Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 17:14:51 -0500 Subject: [PATCH 16/61] Combine grouped chapters if more are fetched while paginated --- .../database/models/MangaChapterHistory.kt | 2 +- .../tachiyomi/ui/recents/RecentsPresenter.kt | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index cf8f5621e646..2969d05802a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -7,7 +7,7 @@ package eu.kanade.tachiyomi.data.database.models * @param chapter object containing chater * @param history object containing history */ -data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, val extraChapters: List = emptyList()) { +data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List = emptyList()) { val allChapters: List get() = listOf(chapter) + extraChapters diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 3f557036ba7d..17984dc0f0bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -161,6 +161,7 @@ class RecentsPresenter( val isCustom = customViewType != null val isEndless = isUngrouped && limit != 0 + var extraCount = 0 val cReading = when { viewType <= VIEW_TYPE_UNGROUP_ALL -> { db.getAllRecentsTypes( @@ -195,11 +196,29 @@ class RecentsPresenter( val date = it.chapter.date_fetch it.manga.id to if (date <= 0L) "-1" else dateFormat.format(Date(date)) } - .map { entry -> + .mapNotNull { entry -> val manga = entry.value.first().manga val chapters = entry.value.map(MangaChapter::chapter) val firstChapter: Chapter var sortedChapters: MutableList + val existingItem = recentItems.find { + val date = Date(it.chapter.date_fetch) + entry.key == it.manga_id to dateFormat.format(date) + }?.takeIf { updatePageCount } + if (existingItem != null) { + extraCount += chapters.size + val newChapters = existingItem.mch.extraChapters + chapters + val sort: Comparator = + ChapterSort(manga, chapterFilter, preferences) + .sortComparator(true) + sortedChapters = newChapters.sortedWith(sort).toMutableList() + sortedChapters = ( + sortedChapters.filter { !it.read } + + sortedChapters.filter { it.read }.reversed() + ).toMutableList() + existingItem.mch.extraChapters = sortedChapters + return@mapNotNull null + } if (chapters.size == 1) { firstChapter = chapters.first() sortedChapters = mutableListOf() @@ -227,14 +246,14 @@ class RecentsPresenter( else -> emptyList() } - if (cReading.size + cReading.sumOf { it.extraChapters.size } < ENDLESS_LIMIT) { + if (cReading.size + cReading.sumOf { it.extraChapters.size } + extraCount < ENDLESS_LIMIT) { finished = true } if (!isCustom && (pageOffset == 0 || updatePageCount) ) { - pageOffset += cReading.size + cReading.sumOf { it.extraChapters.size } + pageOffset += cReading.size + cReading.sumOf { it.extraChapters.size } + extraCount } if (query != oldQuery) return @@ -349,7 +368,7 @@ class RecentsPresenter( } else { heldItems[customViewType] = newItems } - val newCount = itemCount + newItems.size + newItems.sumOf { it.mch.extraChapters.size } + val newCount = itemCount + newItems.size + newItems.sumOf { it.mch.extraChapters.size } + extraCount val hasNewItems = newItems.isNotEmpty() if (updatePageCount && (newCount < if (limit > 0) limit else 25) && (viewType != VIEW_TYPE_GROUP_ALL || query.isNotEmpty()) && From c4a59fd711d4997d5eb95e3bc136f9bc9fab65cd Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Wed, 1 Mar 2023 17:26:44 -0500 Subject: [PATCH 17/61] fixed collapsing/expanding state in recents update when resuming app --- .../java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 7cd946ce0d2b..958891b9346f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -661,7 +661,8 @@ class RecentsController(bundle: Bundle? = null) : override fun updateExpandedExtraChapters(position: Int, expanded: Boolean) { val item = (adapter.getItem(position) as? RecentMangaItem) ?: return val date = presenter.dateFormat.format(item.chapter.date_fetch) - presenter.expandedSectionsMap["${item.mch.manga} - $date"] = expanded + val invertDefault = !adapter.collapseGroupedUpdates + presenter.expandedSectionsMap["${item.mch.manga} - $date"] = expanded.xor(invertDefault) } fun tempJumpTo(viewType: Int) { From d2d650da70eee01bde2a57c6abc6ec7536210916 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 00:06:49 -0500 Subject: [PATCH 18/61] fix opening recents from widget from cold app start --- app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index cb4c1eb97bd9..8045bb592683 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -862,7 +862,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi } else { router.popToRoot() } - if (intent.action == SHORTCUT_RECENTS) return false + if (intent.action == SHORTCUT_RECENTS) return true nav.post { val controller = router.backstack.firstOrNull()?.controller as? RecentsController From b7fc06d2ad7aa96851750d57fad168cd646cc9c4 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 01:17:42 -0500 Subject: [PATCH 19/61] Replace RxJava in reader transitions Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com> --- .../ui/reader/model/ReaderChapter.kt | 17 ++---- .../viewer/pager/PagerTransitionHolder.kt | 52 ++++++++---------- .../viewer/webtoon/WebtoonTransitionHolder.kt | 55 ++++++++----------- 3 files changed, 54 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt index b0a69369c8c1..92c06a138d24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt @@ -1,23 +1,19 @@ package eu.kanade.tachiyomi.ui.reader.model -import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber data class ReaderChapter(val chapter: Chapter) { - var state: State = - State.Wait + val stateFlow = MutableStateFlow(State.Wait) + var state: State + get() = stateFlow.value set(value) { - field = value - stateRelay.call(value) + stateFlow.value = value } - private val stateRelay by lazy { BehaviorRelay.create(state) } - - val stateObserver by lazy { stateRelay.asObservable() } - val pages: List? get() = (state as? State.Loaded)?.pages @@ -25,8 +21,7 @@ data class ReaderChapter(val chapter: Chapter) { var requestedPage: Int = 0 - var references = 0 - private set + private var references = 0 fun ref() { references++ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 71b162d7e36e..0af45b27e731 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -17,8 +17,10 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.widget.ViewPagerAdapter -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * View of the ViewPager that contains a chapter transition. @@ -29,17 +31,15 @@ class PagerTransitionHolder( val transition: ChapterTransition, ) : LinearLayout(viewer.activity), ViewPagerAdapter.PositionableView { + private val scope = MainScope() + private var stateJob: Job? = null + /** * Item that identifies this view. Needed by the adapter to not recreate views. */ override val item: Any get() = transition - /** - * Subscription for status changes of the transition page. - */ - private var statusSubscription: Subscription? = null - /** * View container of the current status of the transition page. Child views will be added * dynamically. @@ -55,11 +55,13 @@ class PagerTransitionHolder( gravity = Gravity.CENTER val sidePadding = 64.dpToPx setPadding(sidePadding, 0, sidePadding, 0) + val transitionView = ReaderTransitionView(context) addView(transitionView) addView(pagesContainer) - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.state.value.manga) + transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transition.to?.let { observeStatus(it) } if (viewer.config.hingeGapSize > 0) { @@ -74,8 +76,7 @@ class PagerTransitionHolder( */ override fun onDetachedFromWindow() { super.onDetachedFromWindow() - statusSubscription?.unsubscribe() - statusSubscription = null + stateJob?.cancel() } /** @@ -83,18 +84,20 @@ class PagerTransitionHolder( * state, the pages container is cleaned up before setting the new state. */ private fun observeStatus(chapter: ReaderChapter) { - statusSubscription?.unsubscribe() - statusSubscription = chapter.stateObserver - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { state -> - pagesContainer.removeAllViews() - when (state) { - is ReaderChapter.State.Wait -> {} - is ReaderChapter.State.Loading -> setLoading() - is ReaderChapter.State.Error -> setError(state.error) - is ReaderChapter.State.Loaded -> setLoaded() + stateJob?.cancel() + stateJob = scope.launch { + chapter.stateFlow + .collectLatest { state -> + pagesContainer.removeAllViews() + when (state) { + is ReaderChapter.State.Loading -> setLoading() + is ReaderChapter.State.Error -> setError(state.error) + is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { + // No additional view is added + } + } } - } + } } /** @@ -112,13 +115,6 @@ class PagerTransitionHolder( pagesContainer.addView(textView) } - /** - * Sets the loaded state on the pages container. - */ - private fun setLoaded() { - // No additional view is added - } - /** * Sets the error state on the pages container. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 5131552f4c35..c2bc963e5b0c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -8,14 +8,17 @@ import android.widget.LinearLayout import android.widget.ProgressBar import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView import eu.kanade.tachiyomi.util.system.dpToPx -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * Holder of the webtoon viewer that contains a chapter transition. @@ -25,10 +28,8 @@ class WebtoonTransitionHolder( viewer: WebtoonViewer, ) : WebtoonBaseHolder(layout, viewer) { - /** - * Subscription for status changes of the transition page. - */ - private var statusSubscription: Subscription? = null + private val scope = MainScope() + private var stateJob: Job? = null private val transitionView = ReaderTransitionView(context) @@ -64,7 +65,8 @@ class WebtoonTransitionHolder( * Binds the given [transition] with this view holder, subscribing to its state. */ fun bind(transition: ChapterTransition) { - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.state.value.manga) + transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transition.to?.let { observeStatus(it, transition) } } @@ -72,7 +74,7 @@ class WebtoonTransitionHolder( * Called when the view is recycled and being added to the view pool. */ override fun recycle() { - unsubscribeStatus() + stateJob?.cancel() } /** @@ -80,30 +82,21 @@ class WebtoonTransitionHolder( * state, the pages container is cleaned up before setting the new state. */ private fun observeStatus(chapter: ReaderChapter, transition: ChapterTransition) { - unsubscribeStatus() - - statusSubscription = chapter.stateObserver - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { state -> - pagesContainer.removeAllViews() - when (state) { - is ReaderChapter.State.Wait -> {} - is ReaderChapter.State.Loading -> setLoading() - is ReaderChapter.State.Error -> setError(state.error, transition) - is ReaderChapter.State.Loaded -> setLoaded() + stateJob?.cancel() + stateJob = scope.launch { + chapter.stateFlow + .collectLatest { state -> + pagesContainer.removeAllViews() + when (state) { + is ReaderChapter.State.Loading -> setLoading() + is ReaderChapter.State.Error -> setError(state.error, transition) + is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> { + // No additional view is added + } + } + pagesContainer.isVisible = pagesContainer.isNotEmpty() } - pagesContainer.isVisible = pagesContainer.childCount > 0 - } - - addSubscription(statusSubscription) - } - - /** - * Unsubscribes from the status subscription. - */ - private fun unsubscribeStatus() { - removeSubscription(statusSubscription) - statusSubscription = null + } } /** From 9c521e23ce14be1325f139de86571e09f33cb55c Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 01:45:48 -0500 Subject: [PATCH 20/61] Replace RxJava in webtoonholder --- .../viewer/webtoon/WebtoonPageHolder.kt | 101 ++++++------------ 1 file changed, 35 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 56f570f44f53..03e98ba2fa85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -23,14 +23,15 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.withIOContext +import eu.kanade.tachiyomi.util.system.withUIContext import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.suspendCancellableCoroutine import java.io.BufferedInputStream import java.io.InputStream @@ -85,22 +86,11 @@ class WebtoonPageHolder( */ private var loadJob: Job? = null - /** - * Job for status changes of the page. - */ - private var statusJob: Job? = null - /** * Job for progress changes of the page. */ private var progressJob: Job? = null - /** - * Subscription 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 - init { refreshLayoutParams() frame.setBackgroundColor(Color.BLACK) @@ -136,7 +126,6 @@ class WebtoonPageHolder( override fun recycle() { cancelLoadJob() cancelProgressJob() - unsubscribeReadImageHeader() removeDecodeErrorLayout() frame.recycle() @@ -150,13 +139,14 @@ class WebtoonPageHolder( */ private fun launchLoadJob() { cancelLoadJob() + loadJob = scope.launch { loadPageAndProcessStatus() } + } + private suspend fun loadPageAndProcessStatus() { val page = page ?: return val loader = page.chapter.pageLoader ?: return - loadJob = scope.launch { - loader.loadPage(page) - } - statusJob = scope.launch { + supervisorScope { + launchIO { loader.loadPage(page) } page.statusFlow.collectLatest { processStatus(it) } } } @@ -168,7 +158,6 @@ class WebtoonPageHolder( cancelProgressJob() val page = page ?: return - progressJob = scope.launch { page.progressFlow.collectLatest { value -> progressBar.setProgress(value) } } @@ -179,7 +168,7 @@ class WebtoonPageHolder( * * @param status the new status of the page. */ - private fun processStatus(status: Page.State) { + private suspend fun processStatus(status: Page.State) { when (status) { Page.State.QUEUE -> setQueued() Page.State.LOAD_PAGE -> setLoading() @@ -203,9 +192,6 @@ class WebtoonPageHolder( */ private fun cancelLoadJob() { loadJob?.cancel() - loadJob = null - statusJob?.cancel() - statusJob = null } /** @@ -213,15 +199,6 @@ class WebtoonPageHolder( */ private fun cancelProgressJob() { progressJob?.cancel() - progressJob = null - } - - /** - * Unsubscribes from the read image header subscription. - */ - private fun unsubscribeReadImageHeader() { - removeSubscription(readImageHeaderSubscription) - readImageHeaderSubscription = null } /** @@ -257,48 +234,40 @@ class WebtoonPageHolder( /** * Called when the page is ready. */ - private fun setImage() { + private suspend fun setImage() { progressContainer.isVisible = true progressBar.isVisible = true progressBar.completeAndFadeOut() retryContainer?.isVisible = false removeDecodeErrorLayout() - unsubscribeReadImageHeader() val streamFn = page?.stream ?: return - var openStream: InputStream? = null - readImageHeaderSubscription = Observable - .fromCallable { - val stream = streamFn().buffered(16) - openStream = process(stream) + val (openStream, isAnimated) = withIOContext { + val stream = streamFn().buffered(16) + val openStream = process(stream) - ImageUtil.isAnimatedAndSupported(stream) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { isAnimated -> - frame.setImage( - openStream!!, - isAnimated, - ReaderPageImageView.Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, - cropBorders = - if (viewer.hasMargins) { - viewer.config.verticalCropBorders - } else { - viewer.config.webtoonCropBorders - }, - ), - ) - } - // Keep the Rx stream alive to close the input stream only when unsubscribed - .flatMap { Observable.never() } - .doOnUnsubscribe { openStream?.close() } - .subscribe({}, {}) + val isAnimated = ImageUtil.isAnimatedAndSupported(stream) + Pair(openStream, isAnimated) + } - addSubscription(readImageHeaderSubscription) + withUIContext { + frame.setImage( + openStream, + isAnimated, + ReaderPageImageView.Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, + cropBorders = viewer.config.run { + if (viewer.hasMargins) { verticalCropBorders } else { webtoonCropBorders } + }, + ), + ) + } + // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { openStream.close() } + } } private fun process(imageStream: BufferedInputStream): InputStream { From 991d46c8aa27ed52cc12fc099a2cc65942ae2149 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 01:48:30 -0500 Subject: [PATCH 21/61] Replace RxJava in SettingsAdvancedController --- .../ui/setting/SettingsAdvancedController.kt | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index b09e10359f93..c715009713cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.openInBrowser import eu.kanade.tachiyomi.util.view.withFadeTransaction import kotlinx.coroutines.CoroutineStart @@ -56,9 +57,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import okhttp3.Headers import rikka.sui.Sui -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -410,25 +408,16 @@ class SettingsAdvancedController : SettingsController() { private fun clearChapterCache() { if (activity == null) return - val files = chapterCache.cacheDir.listFiles() ?: return - - var deletedFiles = 0 - - Observable.defer { Observable.from(files) } - .doOnNext { file -> - if (chapterCache.removeFileFromCache(file.name)) { - deletedFiles++ + viewScope.launchIO { + val files = chapterCache.cacheDir.listFiles() ?: return@launchIO + var deletedFiles = 0 + try { + files.forEach { file -> + if (chapterCache.removeFileFromCache(file.name)) { + deletedFiles++ + } } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - }, - { - activity?.toast(R.string.cache_delete_error) - }, - { + withUIContext { activity?.toast( resources?.getQuantityString( R.plurals.cache_cleared, @@ -438,8 +427,13 @@ class SettingsAdvancedController : SettingsController() { ) findPreference(CLEAR_CACHE_KEY)?.summary = resources?.getString(R.string.used_, chapterCache.readableSize) - }, - ) + } + } catch (_: Exception) { + withUIContext { + activity?.toast(R.string.cache_delete_error) + } + } + } } private fun clearWebViewData() { From 5e444363611322c64c169e7ada8ed53b78002f9b Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 14:15:04 -0500 Subject: [PATCH 22/61] Fix download status not updating in recents --- .../data/database/models/MangaChapterHistory.kt | 2 -- .../kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt | 2 +- .../eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt | 10 ++++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index 2969d05802a7..b022e084158e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -9,8 +9,6 @@ package eu.kanade.tachiyomi.data.database.models */ data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List = emptyList()) { - val allChapters: List - get() = listOf(chapter) + extraChapters companion object { fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt index dcc3077e5bd2..44f70e25db90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaAdapter.kt @@ -62,7 +62,7 @@ class RecentMangaAdapter(val delegate: RecentsInterface) : fun getItemByChapterId(id: Long): RecentMangaItem? { return currentItems.find { val item = (it as? RecentMangaItem) ?: return@find false - return@find id in item.mch.allChapters.map { ch -> ch.id } + return@find id == item.chapter.id || id in item.mch.extraChapters.map { ch -> ch.id } } as? RecentMangaItem } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 17984dc0f0bf..2a0608764688 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -447,7 +447,10 @@ class RecentsPresenter( } override fun updateDownload(download: Download) { - recentItems.find { download.chapter.id in it.mch.allChapters.map { ch -> ch.id } }?.apply { + recentItems.find { + download.chapter.id == it.chapter.id || + download.chapter.id in it.mch.extraChapters.map { ch -> ch.id } + }?.apply { if (chapter.id != download.chapter.id) { val downloadInfo = downloadInfo.find { it.chapterId == download.chapter.id } ?: return@apply @@ -501,7 +504,10 @@ class RecentsPresenter( downloadManager.deleteChapters(listOf(chapter), manga, source, true) } if (update) { - val item = recentItems.find { chapter.id in it.mch.allChapters.map { ch -> ch.id } } ?: return + val item = recentItems.find { + chapter.id == it.chapter.id || + chapter.id in it.mch.extraChapters.map { ch -> ch.id } + } ?: return item.apply { if (chapter.id != item.chapter.id) { val extraChapter = mch.extraChapters.find { it.id == chapter.id } ?: return@apply From 9a48186d7feeb0fa925f1f378b40cd3390b0b847 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Thu, 2 Mar 2023 17:17:20 -0500 Subject: [PATCH 23/61] Update & unify error layout in reader matches upstream, few extra changes with it though also refactoring on the material button main theme Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> --- .../ui/manga/MangaDetailsController.kt | 4 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 2 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 4 +- .../ui/reader/viewer/ReaderButton.kt | 31 ++++ .../ui/reader/viewer/ReaderErrorView.kt | 40 ++++++ .../ui/reader/viewer/pager/PagerButton.kt | 24 ---- .../ui/reader/viewer/pager/PagerPageHolder.kt | 116 +++------------ .../viewer/pager/PagerTransitionHolder.kt | 6 +- .../viewer/webtoon/WebtoonPageHolder.kt | 134 ++++-------------- .../source/browse/BrowseSourceController.kt | 2 +- .../tachiyomi/ui/webview/WebViewActivity.kt | 18 ++- .../layout-sw600dp-land/manga_header_item.xml | 1 - .../layout-sw600dp-port/manga_header_item.xml | 1 - .../res/layout/chapter_sort_bottom_sheet.xml | 1 - app/src/main/res/layout/edit_manga_dialog.xml | 6 +- .../res/layout/extension_detail_header.xml | 1 - app/src/main/res/layout/manga_header_item.xml | 1 - .../main/res/layout/pref_account_login.xml | 2 +- app/src/main/res/layout/reader_error.xml | 31 ++++ .../main/res/layout/set_categories_sheet.xml | 1 - .../main/res/layout/source_filter_sheet.xml | 1 - app/src/main/res/values/styles.xml | 2 +- app/src/main/res/values/themes.xml | 1 + 23 files changed, 177 insertions(+), 253 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorView.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt create mode 100644 app/src/main/res/layout/reader_error.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 7377a6254464..f694019d866e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -1221,8 +1221,8 @@ class MangaDetailsController : val activity = activity ?: return val intent = WebViewActivity.newIntent( activity.applicationContext, - source.id, url, + source.id, presenter.manga .title, ) @@ -1237,8 +1237,8 @@ class MangaDetailsController : val activity = activity ?: return val intent = WebViewActivity.newIntent( activity.applicationContext, - source.id, url, + source.id, presenter.manga .title, ) 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 12487f41f1b2..b16286fa427f 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 @@ -1782,8 +1782,8 @@ class ReaderActivity : BaseActivity() { val intent = WebViewActivity.newIntent( applicationContext, - source.id, chapterUrl, + source.id, viewModel.manga!!.title, ) startActivity(intent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index d3e808ffc5b2..58c3a70e21a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -633,10 +633,10 @@ class ReaderViewModel( return state.value.viewerChapters?.currChapter } - fun getChapterUrl(): String? { + fun getChapterUrl(mainChapter: Chapter? = null): String? { val manga = manga ?: return null val source = getSource() ?: return null - val chapter = getCurrentChapter()?.chapter ?: return null + val chapter = mainChapter ?: getCurrentChapter()?.chapter ?: return null val chapterUrl = try { source.getChapterUrl(chapter) } catch (_: Exception) { null } return chapterUrl.takeIf { !it.isNullOrBlank() } ?: source.getChapterUrl(manga, chapter) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt new file mode 100644 index 000000000000..81505098d4bb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderButton.kt @@ -0,0 +1,31 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import com.google.android.material.button.MaterialButton +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer + +/** + * A button class to be used by child views of the pager viewer. All tap gestures are handled by + * the pager, but this class disables that behavior to allow clickable buttons. + */ +class ReaderButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.materialButtonStyle, +) : MaterialButton(context, attrs, defStyleAttr) { + + var viewer: PagerViewer? = null + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + viewer?.pager?.setGestureDetectorEnabled(false) + if (event.actionMasked == MotionEvent.ACTION_UP) { + viewer?.pager?.setGestureDetectorEnabled(true) + } + return super.onTouchEvent(event) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorView.kt new file mode 100644 index 000000000000..e0815999fa2c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorView.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import androidx.core.view.isVisible +import eu.kanade.tachiyomi.databinding.ReaderErrorBinding +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer +import eu.kanade.tachiyomi.ui.webview.WebViewActivity + +class ReaderErrorView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + LinearLayout(context, attrs) { + lateinit var binding: ReaderErrorBinding + private set + + var viewer: PagerViewer? = null + set(value) { + field = value + binding.actionRetry.viewer = viewer + binding.actionOpenInWebView.viewer = viewer + } + + override fun onFinishInflate() { + super.onFinishInflate() + binding = ReaderErrorBinding.bind(this) + } + + fun configureView(url: String?): ReaderErrorBinding { + if (url?.startsWith("http", true) == true) { + binding.actionOpenInWebView.isVisible = true + binding.actionOpenInWebView.setOnClickListener { + context.startActivity(WebViewActivity.newIntent(context, url)) + } + } else { + binding.actionOpenInWebView.isVisible = false + } + binding.root.isVisible = true + return binding + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt deleted file mode 100644 index 57cef1c8a01a..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerButton.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.viewer.pager - -import android.annotation.SuppressLint -import android.content.Context -import android.view.MotionEvent -import androidx.appcompat.widget.AppCompatButton - -/** - * A button class to be used by child views of the pager viewer. All tap gestures are handled by - * the pager, but this class disables that behavior to allow clickable buttons. - */ -@SuppressLint("ViewConstructor") -class PagerButton(context: Context, viewer: PagerViewer) : AppCompatButton(context) { - - init { - setOnTouchListener { _, event -> - viewer.pager.setGestureDetectorEnabled(false) - if (event.actionMasked == MotionEvent.ACTION_UP) { - viewer.pager.setGestureDetectorEnabled(true) - } - false - } - } -} 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 140a1c26cb3c..d396bd6e58c2 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.annotation.SuppressLint import android.content.Context -import android.content.Intent import android.content.res.ColorStateList import android.graphics.BitmapFactory import android.graphics.Color @@ -12,18 +11,16 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build import android.view.Gravity -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.net.toUri +import android.view.LayoutInflater import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.databinding.ReaderErrorBinding import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderErrorView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType @@ -75,14 +72,9 @@ class PagerPageHolder( private val progressBar = createProgressBar() /** - * Retry button used to allow retrying. + * Error layout to show when the image fails to load. */ - private var retryButton: PagerButton? = null - - /** - * Error layout to show when the image fails to decode. - */ - private var decodeErrorLayout: ViewGroup? = null + private var errorLayout: ReaderErrorView? = null /** * Job for loading the page. @@ -450,8 +442,7 @@ class PagerPageHolder( */ private fun setQueued() { progressBar.isVisible = true - retryButton?.isVisible = false - decodeErrorLayout?.isVisible = false + errorLayout?.isVisible = false } /** @@ -459,8 +450,7 @@ class PagerPageHolder( */ private fun setLoading() { progressBar.isVisible = true - retryButton?.isVisible = false - decodeErrorLayout?.isVisible = false + errorLayout?.isVisible = false } /** @@ -468,8 +458,7 @@ class PagerPageHolder( */ private fun setDownloading() { progressBar.isVisible = true - retryButton?.isVisible = false - decodeErrorLayout?.isVisible = false + errorLayout?.isVisible = false } /** @@ -482,8 +471,7 @@ class PagerPageHolder( } else { progressBar.setProgress(95) } - retryButton?.isVisible = false - decodeErrorLayout?.isVisible = false + errorLayout?.isVisible = false cancelReadImageHeader() val streamFn = page.stream ?: return @@ -583,7 +571,7 @@ class PagerPageHolder( */ private fun setError() { progressBar.isVisible = false - initRetryButton().isVisible = true + showErrorLayout(false) } /** @@ -598,7 +586,7 @@ class PagerPageHolder( */ private fun onImageDecodeError() { progressBar.isVisible = false - initDecodeErrorLayout().isVisible = true + showErrorLayout(true) } /** @@ -621,82 +609,20 @@ class PagerPageHolder( } } - /** - * Initializes a button to retry pages. - */ - private fun initRetryButton(): PagerButton { - if (retryButton != null) return retryButton!! - - retryButton = PagerButton(context, viewer).apply { - layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - gravity = Gravity.CENTER - } - setText(R.string.retry) - setOnClickListener { + private fun showErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding { + if (errorLayout == null) { + errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true).root + errorLayout?.viewer = viewer + errorLayout?.binding?.actionRetry?.setOnClickListener { page.chapter.pageLoader?.retryPage(page) - extraPage?.let { - it.chapter.pageLoader?.retryPage(it) - } } } - addView(retryButton) - return retryButton!! - } - - /** - * Initializes a decode error layout. - */ - private fun initDecodeErrorLayout(): ViewGroup { - if (decodeErrorLayout != null) return decodeErrorLayout!! - - val margins = 8.dpToPx - - val decodeLayout = LinearLayout(context).apply { - gravity = Gravity.CENTER - orientation = LinearLayout.VERTICAL - } - decodeErrorLayout = decodeLayout - - TextView(context).apply { - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(margins, margins, margins, margins) - } - gravity = Gravity.CENTER - setText(R.string.decode_image_error) - - decodeLayout.addView(this) - } - - PagerButton(context, viewer).apply { - layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(margins, margins, margins, margins) - } - setText(R.string.retry) - setOnClickListener { - page.chapter.pageLoader?.retryPage(page) - } - - decodeLayout.addView(this) - } - - val imageUrl = page.imageUrl - if (imageUrl != null && imageUrl.startsWith("http", true)) { - PagerButton(context, viewer).apply { - layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(margins, margins, margins, margins) - } - setText(R.string.open_in_browser) - setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW, imageUrl.toUri()) - context.startActivity(intent) - } - - decodeLayout.addView(this) - } + val imageUrl = if (withOpenInWebView) { + page.imageUrl + } else { + viewer.activity.viewModel.getChapterUrl(page.chapter.chapter) } - - addView(decodeLayout) - return decodeLayout + return errorLayout!!.configureView(imageUrl) } private suspend fun mergeOrSplitPages(imageStream: InputStream, imageStream2: InputStream?): InputStream { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 0af45b27e731..2ec5c876cfb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -14,6 +14,7 @@ import androidx.core.view.updatePaddingRelative import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderButton import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.widget.ViewPagerAdapter @@ -124,13 +125,14 @@ class PagerTransitionHolder( text = context.getString(R.string.failed_to_load_pages_, error.message) } - val retryBtn = PagerButton(context, viewer).apply { + val retryBtn = ReaderButton(context).apply { + viewer = this@PagerTransitionHolder.viewer wrapContent() setText(R.string.retry) setOnClickListener { val toChapter = transition.to if (toChapter != null) { - viewer.activity.requestPreloadChapter(toChapter) + this@PagerTransitionHolder.viewer.activity.requestPreloadChapter(toChapter) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 03e98ba2fa85..1d4ce4946f66 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -1,24 +1,21 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.annotation.SuppressLint -import android.content.Intent import android.content.res.Resources import android.graphics.Color import android.view.Gravity +import android.view.LayoutInflater import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.widget.AppCompatButton -import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.core.view.updatePaddingRelative import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.ReaderErrorBinding import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderErrorView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.util.system.ImageUtil @@ -59,14 +56,9 @@ class WebtoonPageHolder( private lateinit var progressContainer: ViewGroup /** - * Retry button container used to allow retrying. + * Error layout to show when the image fails to load. */ - private var retryContainer: ViewGroup? = null - - /** - * Error layout to show when the image fails to decode. - */ - private var decodeErrorLayout: ViewGroup? = null + private var errorLayout: ReaderErrorView? = null /** * Getter to retrieve the height of the recycler view. @@ -127,7 +119,7 @@ class WebtoonPageHolder( cancelLoadJob() cancelProgressJob() - removeDecodeErrorLayout() + removeErrorLayout() frame.recycle() progressBar.setProgress(0) } @@ -207,8 +199,7 @@ class WebtoonPageHolder( private fun setQueued() { progressContainer.isVisible = true progressBar.isVisible = true - retryContainer?.isVisible = false - removeDecodeErrorLayout() + removeErrorLayout() } /** @@ -217,8 +208,7 @@ class WebtoonPageHolder( private fun setLoading() { progressContainer.isVisible = true progressBar.isVisible = true - retryContainer?.isVisible = false - removeDecodeErrorLayout() + removeErrorLayout() } /** @@ -227,8 +217,7 @@ class WebtoonPageHolder( private fun setDownloading() { progressContainer.isVisible = true progressBar.isVisible = true - retryContainer?.isVisible = false - removeDecodeErrorLayout() + removeErrorLayout() } /** @@ -238,8 +227,7 @@ class WebtoonPageHolder( progressContainer.isVisible = true progressBar.isVisible = true progressBar.completeAndFadeOut() - retryContainer?.isVisible = false - removeDecodeErrorLayout() + removeErrorLayout() val streamFn = page?.stream ?: return @@ -288,7 +276,7 @@ class WebtoonPageHolder( */ private fun setError() { progressContainer.isVisible = false - initRetryLayout().isVisible = true + showErrorLayout(false) } /** @@ -303,7 +291,7 @@ class WebtoonPageHolder( */ private fun onImageDecodeError() { progressContainer.isVisible = false - initDecodeErrorLayout().isVisible = true + showErrorLayout(true) } /** @@ -325,97 +313,25 @@ class WebtoonPageHolder( return progress } - /** - * Initializes a button to retry pages. - */ - private fun initRetryLayout(): ViewGroup { - if (retryContainer != null) return retryContainer!! - - retryContainer = FrameLayout(context) - frame.addView(retryContainer, MATCH_PARENT, parentHeight) - - AppCompatButton(context).apply { - layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - gravity = Gravity.CENTER_HORIZONTAL - setMargins(0, parentHeight / 4, 0, 0) - } - setText(R.string.retry) - setOnClickListener { - page?.let { it.chapter.pageLoader?.retryPage(it) } - } - - retryContainer!!.addView(this) - } - return retryContainer!! - } - - /** - * Initializes a decode error layout. - */ - private fun initDecodeErrorLayout(): ViewGroup { - if (decodeErrorLayout != null) return decodeErrorLayout!! - - val margins = 8.dpToPx - - val decodeLayout = LinearLayout(context).apply { - layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, parentHeight).apply { - setMargins(0, parentHeight / 6, 0, 0) - } - gravity = Gravity.CENTER_HORIZONTAL - orientation = LinearLayout.VERTICAL - } - decodeErrorLayout = decodeLayout - - TextView(context).apply { - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(0, margins, 0, margins) - } - gravity = Gravity.CENTER - setText(R.string.decode_image_error) - - decodeLayout.addView(this) - } - - AppCompatButton(context).apply { - layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(0, margins, 0, margins) - } - setText(R.string.retry) - setOnClickListener { + private fun showErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding { + if (errorLayout == null) { + errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true).root + errorLayout?.binding?.actionRetry?.setOnClickListener { page?.let { it.chapter.pageLoader?.retryPage(it) } } - - decodeLayout.addView(this) } - - val imageUrl = page?.imageUrl - if (imageUrl != null && imageUrl.startsWith("http")) { - AppCompatButton(context).apply { - layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - setMargins(0, margins, 0, margins) - } - setText(R.string.open_in_browser) - setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW, imageUrl.toUri()) - context.startActivity(intent) - } - - decodeLayout.addView(this) - } + val imageUrl = if (withOpenInWebView) { + page?.imageUrl + } else { + viewer.activity.viewModel.getChapterUrl(page?.chapter?.chapter) } - - frame.addView(decodeLayout) - return decodeLayout + return errorLayout!!.configureView(imageUrl) } - /** - * Removes the decode error layout from the holder, if found. - */ - private fun removeDecodeErrorLayout() { - val layout = decodeErrorLayout - if (layout != null) { - frame.removeView(layout) - decodeErrorLayout = null + private fun removeErrorLayout() { + errorLayout?.let { + frame.removeView(it) + errorLayout = null } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 38ae7ad4de77..d99b70142aab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -491,8 +491,8 @@ open class BrowseSourceController(bundle: Bundle) : val activity = activity ?: return val intent = WebViewActivity.newIntent( activity, - source.id, source.baseUrl, + source.id, source.name, ) startActivity(intent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 771e2c21c38b..e8443411d0b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.system.extensionIntentForText import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast +import timber.log.Timber import uy.kohesive.injekt.injectLazy open class WebViewActivity : BaseWebViewActivity() { @@ -37,7 +38,7 @@ open class WebViewActivity : BaseWebViewActivity() { const val URL_KEY = "url_key" const val TITLE_KEY = "title_key" - fun newIntent(context: Context, sourceId: Long, url: String, title: String?): Intent { + fun newIntent(context: Context, url: String, sourceId: Long? = null, title: String? = null): Intent { val intent = Intent(context, WebViewActivity::class.java) intent.putExtra(SOURCE_KEY, sourceId) intent.putExtra(URL_KEY, url) @@ -59,9 +60,19 @@ open class WebViewActivity : BaseWebViewActivity() { onBackPressedDispatcher.onBackPressed() } if (bundle == null) { - val source = sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource ?: return val url = intent.extras!!.getString(URL_KEY) ?: return - val headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + var headers = emptyMap() + (sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source -> + try { + headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + } catch (e: Exception) { + Timber.e(e, "Failed to build headers") + } + } + + headers["user-agent"]?.let { + binding.webview.settings.userAgentString = it + } binding.webview.webViewClient = object : WebViewClientCompat() { override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { @@ -98,7 +109,6 @@ open class WebViewActivity : BaseWebViewActivity() { } } - binding.webview.settings.userAgentString = source.headers["User-Agent"] binding.webview.loadUrl(url, headers) } } diff --git a/app/src/main/res/layout-sw600dp-land/manga_header_item.xml b/app/src/main/res/layout-sw600dp-land/manga_header_item.xml index 7f905aa6ad3c..cbfe0b3f0cf1 100644 --- a/app/src/main/res/layout-sw600dp-land/manga_header_item.xml +++ b/app/src/main/res/layout-sw600dp-land/manga_header_item.xml @@ -355,7 +355,6 @@ -