From 6d6766a86a1c3ac1d9258a6c7f24100525ca25ad Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Mon, 5 Apr 2021 14:55:22 -0400 Subject: [PATCH] Double pages (#699) * Start of dual pages * Fixes to dual pages + added button to shift Next to add a button to get out of this nightmare * Adding preference to truly support on and off * Set double pages correctly for L2R reading mode * Hide shift double page in single page mode also fix shift into or out of double pages not going to right page * Shift page back on loading a new chapter * Fix back shift when reaching near end of chapter with shifted pages * Fix chapters not being marked as read when last pages are doubled * more fixes * change menu icon and verbage * Fixed going to previous/next chapter when turning on/off double shift sometimes * Fix shift icon for double page in R2L * Adding double page as option in reader sheet * Adding saving/share/setting cover for second page * Add back page indicator * RTL language support for current page in double mode * Changing to double page always moves the current page to first slot Also fix bad snap back from single to double or vice versa * Reset shift icon when going to a new chapter * Add Double pages to reader settings * Add padding to current/total pages text since it gets pretty long with double digits in double pages mode * Option for automatic single/double layout based on orientation With option to change mid read * More fixes * Lotta reworking to make this work Lotta reworking to make this work Seriously a lot of reworking to make this work * Cleanup + Some documentation Because I won't "know my own code" I genuinely do not know what I just coded * Swapped around icons for double page in chapter sheet bar Since it was confusing even me * Fix shifting not updating the ui, nor shifting back to the right page * Fix changing chapters * Fixed Prev Chapter transition page jumping back 1 * Fixed progress spinner for single page loading * Fix the case of a second page shifting for full width while double shift is on It would unshift the pages in this case, requiring another shift back If this sounds confusing or complex, it's because it is Also clean up * Update Initial dual page bg to match reader bg theme * More cleanup * More cleanup + docs + refactoring * String update * more cleanup * Update toolbar ripple based if the menu item is visible * Fix more issues with shifting + next chapter's first page being a double * Cleanup ChapterTransision Checks * Always run shifting logic * Even more optimizing which hopefully didnt break something (prolly did) (spoilers it did I had to undo this local commit) (more spoilers it still will break again) * Fix Retry in double mode * Set gifs as fullpages * Fix going from automatic to single/double page layout * More cleanup, more comments * Fix changing page layout when on a transition page for a manga with a single chapter The edge cases you can find....man * Restore shifted pages on activity recrreation * Adding a "beta" tag to page layout IMO it's absolutely solid and release candidate ready, but you can get away with so much by calling something a "beta" or an "alpha" --- .../data/preference/PreferenceKeys.kt | 2 + .../data/preference/PreferencesHelper.kt | 3 + .../tachiyomi/ui/reader/ReaderActivity.kt | 255 +++++++++++++++--- .../tachiyomi/ui/reader/ReaderPresenter.kt | 13 +- .../ui/reader/model/ChapterTransition.kt | 5 +- .../tachiyomi/ui/reader/model/ReaderPage.kt | 13 +- .../ui/reader/settings/ReaderPagedView.kt | 27 +- .../ui/reader/viewer/ViewerConfig.kt | 1 + .../ui/reader/viewer/pager/PagerConfig.kt | 37 +++ .../ui/reader/viewer/pager/PagerPageHolder.kt | 219 +++++++++++++-- .../ui/reader/viewer/pager/PagerViewer.kt | 96 +++++-- .../reader/viewer/pager/PagerViewerAdapter.kt | 190 ++++++++++++- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 2 +- .../ui/setting/SettingsReaderController.kt | 17 ++ .../tachiyomi/util/lang/StringExtensions.kt | 18 ++ .../util/system/CoroutinesExtensions.kt | 3 + .../tachiyomi/widget/MaterialSpinnerView.kt | 9 +- .../widget/preference/MatPreference.kt | 7 +- .../drawable/ic_book_open_variant_24dp.xml | 9 + .../res/drawable/ic_outline_photo_24dp.xml | 10 + .../res/drawable/ic_outline_save_24dp.xml | 10 + .../res/drawable/ic_outline_share_24dp.xml | 10 + .../drawable/ic_page_next_outline_24dp.xml | 9 + .../ic_page_previous_outline_24dp.xml | 9 + .../main/res/drawable/ic_single_page_24dp.xml | 11 + .../res/drawable/reader_toolbar_ripple.xml | 3 +- app/src/main/res/drawable/rect_ripple.xml | 9 + .../main/res/layout/reader_chapters_sheet.xml | 20 +- app/src/main/res/layout/reader_nav.xml | 6 +- .../main/res/layout/reader_paged_layout.xml | 9 +- app/src/main/res/menu/reader.xml | 5 + app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 20 +- 33 files changed, 948 insertions(+), 115 deletions(-) create mode 100644 app/src/main/res/drawable/ic_book_open_variant_24dp.xml create mode 100644 app/src/main/res/drawable/ic_outline_photo_24dp.xml create mode 100644 app/src/main/res/drawable/ic_outline_save_24dp.xml create mode 100644 app/src/main/res/drawable/ic_outline_share_24dp.xml create mode 100644 app/src/main/res/drawable/ic_page_next_outline_24dp.xml create mode 100644 app/src/main/res/drawable/ic_page_previous_outline_24dp.xml create mode 100644 app/src/main/res/drawable/ic_single_page_24dp.xml create mode 100644 app/src/main/res/drawable/rect_ripple.xml 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 4163cb6b11..e93b4b850d 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 @@ -65,6 +65,8 @@ object PreferenceKeys { const val webtoonNavInverted = "reader_tapping_inverted_webtoon" + const val pageLayout = "page_layout" + const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user" const val showNavigationOverlayNewUserWebtoon = "reader_navigation_overlay_new_user_webtoon" 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 5765e29b95..e38a8c66b5 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 @@ -11,6 +11,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onEach import java.io.File @@ -143,6 +144,8 @@ class PreferencesHelper(val context: Context) { fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, ViewerNavigation.TappingInvertMode.NONE) + fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PageLayout.AUTOMATIC) + fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true) fun showNavigationOverlayNewUserWebtoon() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUserWebtoon, true) 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 07c53aeab6..8faae2f80b 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 @@ -8,10 +8,12 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color +import android.graphics.drawable.LayerDrawable import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.Menu +import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -20,9 +22,10 @@ import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.SeekBar import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils -import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible +import androidx.core.view.GestureDetectorCompat import com.afollestad.materialdialogs.MaterialDialog import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -46,6 +49,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer @@ -57,6 +62,7 @@ import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.hasSideNavBar import eu.kanade.tachiyomi.util.system.isBottomTappable +import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.openInBrowser @@ -160,6 +166,10 @@ class ReaderActivity : var isLoading = false + var lastShiftDoubleState: Boolean? = null + var indexPageToShift: Int? = null + var indexChapterToShift: Long? = null + companion object { @Suppress("unused") const val LEFT_TO_RIGHT = 1 @@ -168,6 +178,10 @@ class ReaderActivity : const val WEBTOON = 4 const val VERTICAL_PLUS = 5 + const val SHIFT_DOUBLE_PAGES = "shiftingDoublePages" + const val SHIFTED_PAGE_INDEX = "shiftedPageIndex" + const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex" + fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { val intent = Intent(context, ReaderActivity::class.java) intent.putExtra("manga", manga.id) @@ -217,6 +231,9 @@ class ReaderActivity : if (savedInstanceState != null) { menuVisible = savedInstanceState.getBoolean(::menuVisible.name) + lastShiftDoubleState = savedInstanceState.get(SHIFT_DOUBLE_PAGES) as? Boolean + indexPageToShift = savedInstanceState.get(SHIFTED_PAGE_INDEX) as? Int + indexChapterToShift = savedInstanceState.get(SHIFTED_CHAP_INDEX) as? Long binding.readerNav.root.isVisible = menuVisible } else { binding.readerNav.root.gone() @@ -251,6 +268,16 @@ class ReaderActivity : */ override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(::menuVisible.name, menuVisible) + (viewer as? PagerViewer)?.let { pViewer -> + val config = pViewer.config + outState.putBoolean(SHIFT_DOUBLE_PAGES, config.shiftDoublePage) + if (config.shiftDoublePage) { + pViewer.getShiftedPage()?.let { + outState.putInt(SHIFTED_PAGE_INDEX, it.index) + outState.putLong(SHIFTED_CHAP_INDEX, it.chapter.chapter.id ?: 0L) + } + } + } if (!isChangingConfigurations) { presenter.onSaveInstanceStateNonConfigurationChange() } @@ -279,6 +306,65 @@ class ReaderActivity : return true } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val splitItem = menu?.findItem(R.id.action_shift_double_page) + splitItem?.isVisible = (viewer as? PagerViewer)?.config?.doublePages ?: false + (viewer as? PagerViewer)?.config?.let { config -> + splitItem?.icon = ContextCompat.getDrawable( + this, + if ((!config.shiftDoublePage).xor(viewer is R2LPagerViewer)) R.drawable.ic_page_previous_outline_24dp else R.drawable.ic_page_next_outline_24dp + ) + } + setBottomNavButtons(preferences.pageLayout().get()) + (binding.toolbar.background as? LayerDrawable)?.let { layerDrawable -> + val isDoublePage = splitItem?.isVisible ?: false + // Shout out to Google for not fixing setVisible https://issuetracker.google.com/issues/127538945 + layerDrawable.findDrawableByLayerId(R.id.layer_full_width).alpha = if (!isDoublePage) 255 else 0 + layerDrawable.findDrawableByLayerId(R.id.layer_one_item).alpha = if (isDoublePage) 255 else 0 + } + return super.onPrepareOptionsMenu(menu) + } + + fun setBottomNavButtons(pageLayout: Int) { + val isDoublePage = pageLayout == PageLayout.DOUBLE_PAGES || + (pageLayout == PageLayout.AUTOMATIC && (viewer as? PagerViewer)?.config?.doublePages ?: false) + binding.chaptersSheet.doublePage.setImageDrawable( + ContextCompat.getDrawable( + this, + if (!isDoublePage) R.drawable.ic_single_page_24dp + else R.drawable.ic_book_open_variant_24dp + ) + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + binding.chaptersSheet.doublePage.tooltipText = + getString( + if (isDoublePage) R.string.switch_to_single + else R.string.switch_to_double + ) + } + } + + /** + * Called when an item of the options menu was clicked. Used to handle clicks on our menu + * entries. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_shift_double_page -> { + (viewer as? PagerViewer)?.config?.let { config -> + config.shiftDoublePage = !config.shiftDoublePage + presenter.viewerChapters?.let { + (viewer as? PagerViewer)?.updateShifting() + (viewer as? PagerViewer)?.setChaptersDoubleShift(it) + invalidateOptionsMenu() + } + } + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + private fun popToMain() { presenter.onBackPressed() if (fromUrl) { @@ -358,6 +444,16 @@ class ReaderActivity : } } + binding.chaptersSheet.doublePage.setOnClickListener { + if (preferences.pageLayout().get() == PageLayout.AUTOMATIC) { + (viewer as? PagerViewer)?.config?.let { config -> + config.doublePages = !config.doublePages + reloadChapters(config.doublePages, true) + } + } else { + preferences.pageLayout().set(1 - preferences.pageLayout().get()) + } + } binding.readerNav.leftChapter.setOnClickListener { if (isLoading) { return@setOnClickListener @@ -579,6 +675,7 @@ class ReaderActivity : binding.viewerContainer.removeAllViews() } viewer = newViewer + binding.chaptersSheet.doublePage.isVisible = viewer is PagerViewer binding.viewerContainer.addView(newViewer.getView()) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -591,6 +688,14 @@ class ReaderActivity : } } + if (newViewer is PagerViewer) { + if (preferences.pageLayout().get() == PageLayout.AUTOMATIC) { + setDoublePageMode(newViewer) + } + lastShiftDoubleState?.let { newViewer.config.shiftDoublePage = it } + lastShiftDoubleState = null + } + binding.navigationOverlay.isLTR = !(viewer is L2RPagerViewer) binding.viewerContainer.setBackgroundColor( if (viewer is WebtoonViewer) { @@ -606,6 +711,7 @@ class ReaderActivity : binding.pleaseWait.visible() binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long)) + invalidateOptionsMenu() } override fun onPause() { @@ -613,12 +719,44 @@ class ReaderActivity : super.onPause() } + fun reloadChapters(doublePages: Boolean, force: Boolean = false) { + val pViewer = viewer as? PagerViewer ?: return + pViewer.updateShifting() + if (!force && pViewer.config.autoDoublePages) { + setDoublePageMode(pViewer) + } else { + pViewer.config.doublePages = doublePages + } + val currentChapter = presenter.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.progress + + ( + currentChapter?.pages?.subList(0, binding.readerNav.pageSeekbar.progress) + ?.count { it.fullPage || it.isolatedPage } ?: 0 + ) + ) % 2 != 0 + } + presenter.viewerChapters?.let { + pViewer.setChaptersDoubleShift(it) + } + invalidateOptionsMenu() + } + /** * Called from the presenter whenever a new [viewerChapters] have been set. It delegates the * method to the current viewer, but also set the subtitle on the binding.toolbar. */ fun setChapters(viewerChapters: ViewerChapters) { binding.pleaseWait.gone() + if (indexChapterToShift != null && indexPageToShift != null) { + viewerChapters.currChapter.pages?.find { it.index == indexPageToShift && it.chapter.chapter.id == indexChapterToShift }?.let { + (viewer as? PagerViewer)?.updateShifting(it) + } + indexChapterToShift = null + indexPageToShift = null + } viewer?.setChapters(viewerChapters) intentPageNumber?.let { moveToPageIndex(it) } intentPageNumber = null @@ -687,58 +825,100 @@ class ReaderActivity : * bottom menu and delegates the change to the presenter. */ @SuppressLint("SetTextI18n") - fun onPageSelected(page: ReaderPage) { - presenter.onPageSelected(page) + fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) { + presenter.onPageSelected(page, hasExtraPage) val pages = page.chapter.pages ?: return - val currentPage = page.number - val totalPages = pages.size - - // Set bottom page number - binding.pageNumber.text = "$currentPage/$totalPages" - - if (viewer is R2LPagerViewer) { - binding.readerNav.rightPageText.text = currentPage.toString() - binding.readerNav.leftPageText.text = totalPages.toString() + val currentPage = if (hasExtraPage) { + if (resources.isLTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}" } else { - binding.readerNav.leftPageText.text = currentPage.toString() - binding.readerNav.rightPageText.text = totalPages.toString() + "${page.number}" + } + + val totalPages = pages.size.toString() + binding.pageNumber.text = if (resources.isLTR) "$currentPage/$totalPages" else "$totalPages/$currentPage" + if (viewer is R2LPagerViewer) { + binding.readerNav.rightPageText.text = currentPage + binding.readerNav.leftPageText.text = totalPages + } else { + binding.readerNav.leftPageText.text = currentPage + binding.readerNav.rightPageText.text = totalPages } if (binding.chaptersSheet.chaptersBottomSheet.selectedChapterId != page.chapter.chapter.id) { binding.chaptersSheet.chaptersBottomSheet.refreshList() } // Set seekbar progress binding.readerNav.pageSeekbar.max = pages.lastIndex - binding.readerNav.pageSeekbar.progress = page.index + val progress = page.index + if (hasExtraPage) 1 else 0 + // For a double page, show the last 2 pages as if it was the final part of the seekbar + binding.readerNav.pageSeekbar.progress = if (progress == pages.lastIndex) progress else page.index } /** * Called from the viewer whenever a [page] is long clicked. A bottom sheet with a list of * actions to perform is shown. */ - fun onPageLongTap(page: ReaderPage) { - val items = listOf( - MaterialMenuSheet.MenuSheetItem( - 0, - R.drawable.ic_share_24dp, - R.string.share - ), - MaterialMenuSheet.MenuSheetItem( - 1, - R.drawable.ic_save_24dp, - R.string.save - ), - MaterialMenuSheet.MenuSheetItem( - 2, - R.drawable.ic_photo_24dp, - R.string.set_as_cover + fun onPageLongTap(page: ReaderPage, extraPage: ReaderPage? = null) { + val items = if (extraPage != null) { + listOf( + MaterialMenuSheet.MenuSheetItem( + 3, + R.drawable.ic_outline_share_24dp, + R.string.share_second_page + ), + MaterialMenuSheet.MenuSheetItem( + 4, + R.drawable.ic_outline_save_24dp, + R.string.save_second_page + ), + MaterialMenuSheet.MenuSheetItem( + 5, + R.drawable.ic_outline_photo_24dp, + R.string.set_second_page_as_cover + ), + MaterialMenuSheet.MenuSheetItem( + 0, + R.drawable.ic_share_24dp, + R.string.share_first_page + ), + MaterialMenuSheet.MenuSheetItem( + 1, + R.drawable.ic_save_24dp, + R.string.save_first_page + ), + MaterialMenuSheet.MenuSheetItem( + 2, + R.drawable.ic_photo_24dp, + R.string.set_first_page_as_cover + ) ) - ) + } else { + listOf( + MaterialMenuSheet.MenuSheetItem( + 0, + R.drawable.ic_share_24dp, + R.string.share + ), + MaterialMenuSheet.MenuSheetItem( + 1, + R.drawable.ic_save_24dp, + R.string.save + ), + MaterialMenuSheet.MenuSheetItem( + 2, + R.drawable.ic_photo_24dp, + R.string.set_as_cover + ) + ) + } MaterialMenuSheet(this, items) { _, item -> when (item) { 0 -> shareImage(page) 1 -> saveImage(page) 2 -> showSetCoverPrompt(page) + 3 -> extraPage?.let { shareImage(it) } + 4 -> extraPage?.let { saveImage(it) } + 5 -> extraPage?.let { showSetCoverPrompt(it) } } true }.show() @@ -911,6 +1091,11 @@ class ReaderActivity : } } + private fun setDoublePageMode(viewer: PagerViewer) { + val currentOrientation = resources.configuration.orientation + viewer.config.doublePages = (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) + } + private fun handleIntentAction(intent: Intent): Boolean { val uri = intent.data ?: return false if (!presenter.canLoadUrl(uri)) { @@ -1000,6 +1185,12 @@ class ReaderActivity : preferences.alwaysShowChapterTransition().asFlow() .onEach { showNewChapter = it } .launchIn(scope) + + preferences.pageLayout().asFlow() + .onEach { + setBottomNavButtons(it) + } + .launchIn(scope) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 1fb872f15a..e9c868c1d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -86,6 +86,9 @@ class ReaderPresenter( */ private val viewerChaptersRelay = BehaviorRelay.create() + val viewerChapters: ViewerChapters? + get() = viewerChaptersRelay.value + /** * Relay used when loading prev/next chapter needed to lock the UI (with a dialog). */ @@ -398,8 +401,7 @@ class ReaderPresenter( .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) } .subscribeFirst( { view, _ -> - val lastPage = if (chapter.pages_left <= 1) 0 else chapter.last_page_read - view.moveToPageIndex(lastPage) + view.moveToPageIndex(0) view.refreshChapters() }, { _, _ -> @@ -458,7 +460,7 @@ class ReaderPresenter( * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this * [page]'s chapter is different from the currently active. */ - fun onPageSelected(page: ReaderPage) { + fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) { val currentChapters = viewerChaptersRelay.value ?: return val selectedChapter = page.chapter @@ -467,7 +469,10 @@ class ReaderPresenter( selectedChapter.chapter.last_page_read = page.index selectedChapter.chapter.pages_left = (selectedChapter.pages?.size ?: page.index) - page.index - if (selectedChapter.pages?.lastIndex == page.index) { + // For double pages, check if the second to last page is doubled up + if (selectedChapter.pages?.lastIndex == page.index || + (hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index) + ) { selectedChapter.chapter.read = true updateTrackChapterRead(selectedChapter) deleteChapterIfNeeded(selectedChapter) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt index 41b6a3d787..47da72bf52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ChapterTransition.kt @@ -17,8 +17,9 @@ sealed class ChapterTransition { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ChapterTransition) return false - if (from == other.from && to == other.to) return true - if (from == other.to && to == other.from) return true + if (from == other.from && to == other.to && to != null) return true + if (from == other.to && to == other.from && to != null) return true + if (to == other.to && to == null && from == other.from && other::class == this::class) return true return false } 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 fbca23521a..30bc4faa1f 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 @@ -10,8 +10,19 @@ class ReaderPage( imageUrl: String? = null, var stream: (() -> InputStream)? = null, var bg: Drawable? = null, - var bgType: Int? = null + var bgType: Int? = null, + /** Value to check if this page is used to as if it was too wide */ + var shiftedPage: Boolean = false, + /** Value to check if a page is can be doubled up, but can't because the next page is too wide */ + var isolatedPage: Boolean = false ) : Page(index, url, imageUrl, null) { lateinit var chapter: ReaderChapter + + /** Value to check if a page is too wide to be doubled up */ + var fullPage: Boolean = false + set(value) { + field = value + if (value) shiftedPage = false + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/settings/ReaderPagedView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/settings/ReaderPagedView.kt index 1ed952569d..d00d58c95d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/settings/ReaderPagedView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/settings/ReaderPagedView.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.bindToPreference +import eu.kanade.tachiyomi.util.lang.addBetaTag import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.widget.BaseReaderSettingsView @@ -27,12 +28,18 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu binding.pagerNav.bindToPreference(preferences.navigationModePager()) binding.pagerInvert.bindToPreference(preferences.pagerNavInverted()) binding.extendPastCutout.bindToPreference(preferences.pagerCutoutBehavior()) + binding.pageLayout.bindToPreference(preferences.pageLayout()) + + binding.pageLayout.title = binding.pageLayout.title.toString().addBetaTag(context) val mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaViewer() ?: 0 val isWebtoonView = mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS val hasMargins = mangaViewer == ReaderActivity.VERTICAL_PLUS binding.cropBordersWebtoon.bindToPreference(if (hasMargins) preferences.cropBorders() else preferences.cropBordersWebtoon()) - binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) + binding.webtoonSidePadding.bindToIntPreference( + preferences.webtoonSidePadding(), + R.array.webtoon_side_padding_values + ) binding.webtoonEnableZoomOut.bindToPreference(preferences.webtoonEnableZoomOut()) binding.webtoonNav.bindToPreference(preferences.navigationModeWebtoon()) binding.webtoonInvert.bindToPreference(preferences.webtoonNavInverted()) @@ -49,8 +56,22 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu } private fun updatePagedGroup(show: Boolean) { - listOf(binding.scaleType, binding.zoomStart, binding.cropBorders, binding.pageTransitions, binding.pagerNav, binding.pagerInvert).forEach { it.visibleIf(show) } - listOf(binding.cropBordersWebtoon, binding.webtoonSidePadding, binding.webtoonEnableZoomOut, binding.webtoonNav, binding.webtoonInvert).forEach { it.visibleIf(!show) } + listOf( + binding.scaleType, + binding.zoomStart, + binding.cropBorders, + binding.pageTransitions, + binding.pagerNav, + binding.pagerInvert, + binding.pageLayout + ).forEach { it.visibleIf(show) } + listOf( + binding.cropBordersWebtoon, + binding.webtoonSidePadding, + binding.webtoonEnableZoomOut, + binding.webtoonNav, + binding.webtoonInvert + ).forEach { it.visibleIf(!show) } val isFullFit = when (preferences.imageScaleType().get()) { SubsamplingScaleImageView.SCALE_TYPE_FIT_HEIGHT, SubsamplingScaleImageView.SCALE_TYPE_SMART_FIT, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt index fa4cc01ec2..41c4e5cdfd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt @@ -16,6 +16,7 @@ abstract class ViewerConfig(preferences: PreferencesHelper) { protected val scope = CoroutineScope(Job() + Dispatchers.Main) var imagePropertyChangedListener: (() -> Unit)? = null + var reloadChapterListener: ((Boolean) -> Unit)? = null var navigationModeChangedListener: (() -> Unit)? = null var navigationModeInvertedListener: (() -> Unit)? = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index c7c199818e..f1fd44c871 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -38,6 +38,18 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe var cutoutBehavior = 0 private set + var shiftDoublePage = false + + var doublePages = preferences.pageLayout().get() == PageLayout.DOUBLE_PAGES + set(value) { + field = value + if (!value) { + shiftDoublePage = false + } + } + + var autoDoublePages = preferences.pageLayout().get() == PageLayout.AUTOMATIC + init { preferences.pageTransitions() .register({ usePageTransitions = it }) @@ -75,6 +87,25 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe preferences.readerTheme() .register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() }) + preferences.pageLayout() + .asFlow() + .drop(1) + .onEach { + autoDoublePages = it == PageLayout.AUTOMATIC + if (!autoDoublePages) { + doublePages = it == PageLayout.DOUBLE_PAGES + } + reloadChapterListener?.invoke(doublePages) + } + .launchIn(scope) + preferences.pageLayout() + .register({ + autoDoublePages = it == PageLayout.AUTOMATIC + if (!autoDoublePages) { + doublePages = it == PageLayout.DOUBLE_PAGES + } + }) + navigationOverlayForNewUser = preferences.showNavigationOverlayNewUser().get() if (navigationOverlayForNewUser) { preferences.showNavigationOverlayNewUser().set(false) @@ -141,3 +172,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe const val CUTOUT_IGNORE = 2 } } + +object PageLayout { + const val SINGLE_PAGE = 0 + const val DOUBLE_PAGES = 1 + const val AUTOMATIC = 2 +} 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 4bcff62faf..5ad3cdf395 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 @@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Canvas import android.graphics.Color import android.graphics.PointF +import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.GestureDetector import android.view.Gravity @@ -48,8 +51,11 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.InputStream import java.util.concurrent.TimeUnit +import kotlin.math.max /** * View of the ViewPager that contains a page of a chapter. @@ -57,14 +63,15 @@ import java.util.concurrent.TimeUnit @SuppressLint("ViewConstructor") class PagerPageHolder( val viewer: PagerViewer, - val page: ReaderPage + val page: ReaderPage, + private var extraPage: ReaderPage? = null ) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView { /** * Item that identifies this view. Needed by the adapter to not recreate views. */ override val item - get() = page + get() = page to extraPage /** * Loading progress bar to indicate the current progress. @@ -101,12 +108,28 @@ class PagerPageHolder( */ private var progressSubscription: Subscription? = null + /** + * Subscription for status changes of the page. + */ + private var extraStatusSubscription: Subscription? = null + + /** + * Subscription for progress changes of the page. + */ + private var extraProgressSubscription: Subscription? = 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). */ private var readImageHeaderSubscription: Subscription? = null + var status: Int = 0 + var extraStatus: Int = 0 + var progress: Int = 0 + var extraProgress: Int = 0 + private var skipExtra = false + init { addView(progressBar) observeStatus() @@ -124,8 +147,10 @@ class PagerPageHolder( @SuppressLint("ClickableViewAccessibility") override fun onDetachedFromWindow() { super.onDetachedFromWindow() - unsubscribeProgress() - unsubscribeStatus() + unsubscribeProgress(1) + unsubscribeStatus(1) + unsubscribeProgress(2) + unsubscribeStatus(2) unsubscribeReadImageHeader() subsamplingImageView?.setOnImageEventListener(null) } @@ -141,7 +166,18 @@ class PagerPageHolder( val loader = page.chapter.pageLoader ?: return statusSubscription = loader.getPage(page) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { processStatus(it) } + .subscribe { + status = it + processStatus(it) + } + val extraPage = extraPage ?: return + val loader2 = extraPage.chapter.pageLoader ?: return + extraStatusSubscription = loader2.getPage(extraPage) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + extraStatus = it + processStatus2(it) + } } /** @@ -155,7 +191,28 @@ class PagerPageHolder( .distinctUntilChanged() .onBackpressureLatest() .observeOn(AndroidSchedulers.mainThread()) - .subscribe { value -> progressBar.setProgress(value) } + .subscribe { value -> + progress = value + if (extraPage == null) { + progressBar.setProgress(progress) + } else { + progressBar.setProgress((progress + extraProgress) / 2) + } + } + } + + private fun observeProgress2() { + extraProgressSubscription?.unsubscribe() + val extraPage = extraPage ?: return + extraProgressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS) + .map { extraPage.progress } + .distinctUntilChanged() + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { value -> + extraProgress = value + progressBar.setProgress((progress + extraProgress) / 2) + } } /** @@ -172,12 +229,40 @@ class PagerPageHolder( setDownloading() } Page.READY -> { - setImage() - unsubscribeProgress() + if (extraStatus == Page.READY || extraPage == null) { + setImage() + } + unsubscribeProgress(1) } Page.ERROR -> { setError() - unsubscribeProgress() + unsubscribeProgress(1) + } + } + } + + /** + * Called when the status of the page changes. + * + * @param status the new status of the page. + */ + private fun processStatus2(status: Int) { + when (status) { + Page.QUEUE -> setQueued() + Page.LOAD_PAGE -> setLoading() + Page.DOWNLOAD_IMAGE -> { + observeProgress2() + setDownloading() + } + Page.READY -> { + if (this.status == Page.READY) { + setImage() + } + unsubscribeProgress(2) + } + Page.ERROR -> { + setError() + unsubscribeProgress(2) } } } @@ -185,17 +270,19 @@ class PagerPageHolder( /** * Unsubscribes from the status subscription. */ - private fun unsubscribeStatus() { - statusSubscription?.unsubscribe() - statusSubscription = null + private fun unsubscribeStatus(page: Int) { + val subscription = if (page == 1) statusSubscription else extraStatusSubscription + subscription?.unsubscribe() + if (page == 1) statusSubscription = null else extraStatusSubscription = null } /** * Unsubscribes from the progress subscription. */ - private fun unsubscribeProgress() { - progressSubscription?.unsubscribe() - progressSubscription = null + private fun unsubscribeProgress(page: Int) { + val subscription = if (page == 1) progressSubscription else extraProgressSubscription + subscription?.unsubscribe() + if (page == 1) progressSubscription = null else extraProgressSubscription = null } /** @@ -244,23 +331,30 @@ class PagerPageHolder( unsubscribeReadImageHeader() val streamFn = page.stream ?: return + val streamFn2 = extraPage?.stream var openStream: InputStream? = null + readImageHeaderSubscription = Observable .fromCallable { val stream = streamFn().buffered(16) - openStream = stream - ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF + val stream2 = if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null + openStream = this@PagerPageHolder.mergePages(stream, stream2) + ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF || + if (stream2 != null) ImageUtil.findImageType(stream2) == ImageUtil.ImageType.GIF else false } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { isAnimated -> + if (skipExtra) { + onPageSplit() + } if (!isAnimated) { if (viewer.config.readerTheme >= 2) { val imageView = initSubsamplingImageView() if (page.bg != null && - page.bgType == getBGType(viewer.config.readerTheme, context) + page.bgType == getBGType(viewer.config.readerTheme, context) + item.hashCode() ) { imageView.setImage(ImageSource.inputStream(openStream!!)) imageView.background = page.bg @@ -275,7 +369,7 @@ class PagerPageHolder( launchUI { imageView.background = setBG(bytesArray) page.bg = imageView.background - page.bgType = getBGType(viewer.config.readerTheme, context) + page.bgType = getBGType(viewer.config.readerTheme, context) + item.hashCode() } } } else { @@ -293,7 +387,9 @@ class PagerPageHolder( // Keep the Rx stream alive to close the input stream only when unsubscribed .flatMap { Observable.never() } .doOnUnsubscribe { - try { openStream?.close() } catch (e: Exception) {} + try { + openStream?.close() + } catch (e: Exception) {} } .subscribe({}, {}) } @@ -468,6 +564,9 @@ class PagerPageHolder( setText(R.string.retry) setOnClickListener { page.chapter.pageLoader?.retryPage(page) + extraPage?.let { + it.chapter.pageLoader?.retryPage(it) + } } } addView(retryButton) @@ -530,6 +629,83 @@ class PagerPageHolder( return decodeLayout } + private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream { + imageStream2 ?: return imageStream + if (page.fullPage) return imageStream + if (ImageUtil.findImageType(imageStream) == ImageUtil.ImageType.GIF) { + page.fullPage = true + skipExtra = true + return imageStream + } else if (ImageUtil.findImageType(imageStream2) == ImageUtil.ImageType.GIF) { + page.isolatedPage = true + extraPage?.fullPage = true + skipExtra = true + return imageStream + } + val imageBytes = imageStream.readBytes() + + val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val height = imageBitmap.height + val width = imageBitmap.width + + if (height < width) { + imageStream2.close() + imageStream.close() + page.fullPage = true + skipExtra = true + return imageBytes.inputStream() + } + + val imageBytes2 = imageStream2.readBytes() + val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size) + val height2 = imageBitmap2.height + val width2 = imageBitmap2.width + + if (height2 < width2) { + imageStream2.close() + imageStream.close() + extraPage?.fullPage = true + page.isolatedPage = true + skipExtra = true + return imageBytes.inputStream() + } + + val maxHeight = max(height, height2) + + val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888) + val canvas = Canvas(result) + canvas.drawColor(if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) Color.WHITE else Color.BLACK) + val isLTR = viewer !is R2LPagerViewer + val upperPart = Rect( + if (isLTR) 0 else width2, + (maxHeight - imageBitmap.height) / 2, + (if (isLTR) 0 else width2) + imageBitmap.width, + imageBitmap.height + (maxHeight - imageBitmap.height) / 2 + ) + canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null) + val bottomPart = Rect( + if (!isLTR) 0 else width, + (maxHeight - imageBitmap2.height) / 2, + (if (!isLTR) 0 else width) + imageBitmap2.width, + imageBitmap2.height + (maxHeight - imageBitmap2.height) / 2 + ) + canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null) + + val output = ByteArrayOutputStream() + result.compress(Bitmap.CompressFormat.JPEG, 100, output) + imageStream.close() + imageStream2.close() + return ByteArrayInputStream(output.toByteArray()) + } + + private fun onPageSplit() { + extraPage ?: return + viewer.onPageSplit(page) + if (extraPage?.fullPage == true) { + extraPage = null + } + } + /** * Extension method to set a [stream] into this ImageView. */ @@ -541,6 +717,9 @@ class PagerPageHolder( } } + private val Bitmap.rect: Rect + get() = Rect(0, 0, width, height) + companion object { fun getBGType(readerTheme: Int, context: Context): Int { return if (readerTheme == 3) { 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 ecac6baf94..07b60752a8 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 @@ -60,29 +60,29 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { field = value if (value) { awaitingIdleViewerChapters?.let { - setChaptersInternal(it) + setChaptersDoubleShift(it) awaitingIdleViewerChapters = null } } } + private var pagerListener = object : ViewPager.SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + onPageChange(position) + } + + override fun onPageScrollStateChanged(state: Int) { + isIdle = state == ViewPager.SCROLL_STATE_IDLE + } + } + init { pager.gone() // Don't layout the pager yet pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) pager.offscreenPageLimit = 1 pager.id = R.id.reader_pager pager.adapter = adapter - pager.addOnPageChangeListener( - object : ViewPager.SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - onPageChange(position) - } - - override fun onPageScrollStateChanged(state: Int) { - isIdle = state == ViewPager.SCROLL_STATE_IDLE - } - } - ) + pager.addOnPageChangeListener(pagerListener) pager.tapListener = f@{ event -> if (!config.tappingEnabled) { activity.toggleMenu() @@ -101,9 +101,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } pager.longTapListener = f@{ if (activity.menuVisible || config.longTapEnabled) { - val item = adapter.items.getOrNull(pager.currentItem) - if (item is ReaderPage) { - activity.onPageLongTap(item) + val item = adapter.joinedItems.getOrNull(pager.currentItem) + val firstPage = item?.first as? ReaderPage + val secondPage = item?.second as? ReaderPage + if (firstPage is ReaderPage) { + activity.onPageLongTap(firstPage, secondPage) return@f true } } @@ -114,6 +116,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { refreshAdapter() } + config.reloadChapterListener = { + activity.reloadChapters(it) + } + config.navigationModeChangedListener = { val showOnStart = config.navigationOverlayForNewUser activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) @@ -136,14 +142,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { /** * Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active */ - private fun onPageChange(position: Int) { - val page = adapter.items.getOrNull(position) + fun onPageChange(position: Int) { + val page = adapter.joinedItems.getOrNull(position) if (page != null && currentPage != page) { - val allowPreload = checkAllowPreload(page as? ReaderPage) - currentPage = page - when (page) { - is ReaderPage -> onReaderPageSelected(page, allowPreload) - is ChapterTransition -> onTransitionSelected(page) + val allowPreload = checkAllowPreload(page.first as? ReaderPage) + currentPage = page.first + when (val aPage = page.first) { + is ReaderPage -> onReaderPageSelected(aPage, allowPreload, page.second != null) + is ChapterTransition -> onTransitionSelected(aPage) } } } @@ -171,11 +177,16 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { * Called when a [ReaderPage] is marked as active. It notifies the * activity of the change and requests the preload of the next chapter if this is the last page. */ - private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) { - activity.onPageSelected(page) + private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, hasExtraPage: Boolean) { + activity.onPageSelected(page, hasExtraPage) + val offset = if (hasExtraPage) 1 else 0 val pages = page.chapter.pages ?: return - Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") + if (hasExtraPage) { + Timber.d("onReaderPageSelected: ${page.number}-${page.number + offset}/${pages.size}") + } else { + Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") + } // Preload next chapter once we're within the last 5 pages of the current chapter val inPreloadRange = pages.size - page.number < 5 if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) { @@ -202,13 +213,29 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { } } + fun setChaptersDoubleShift(chapters: ViewerChapters) { + // Remove Listener since we're about to change the size of the items + // 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 + onPageChange(pager.currentItem) + } + + fun updateShifting(page: ReaderPage? = null) { + adapter.pageToShift = page ?: adapter.joinedItems[pager.currentItem].first as? ReaderPage + } + + fun getShiftedPage(): ReaderPage? = adapter.pageToShift + /** * Tells this viewer to set the given [chapters] as active. If the pager is currently idle, * it sets the chapters immediately, otherwise they are saved and set when it becomes idle. */ override fun setChapters(chapters: ViewerChapters) { if (isIdle) { - setChaptersInternal(chapters) + setChaptersDoubleShift(chapters) } else { awaitingIdleViewerChapters = chapters } @@ -219,7 +246,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { */ private fun setChaptersInternal(chapters: ViewerChapters) { Timber.d("setChaptersInternal") - val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull( + val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull( pager .currentItem ) is ChapterTransition @@ -232,6 +259,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { moveToPage(pages[chapters.currChapter.requestedPage]) pager.visible() } + activity.invalidateOptionsMenu() } /** @@ -239,13 +267,21 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { */ override fun moveToPage(page: ReaderPage) { Timber.d("moveToPage ${page.number}") - val position = adapter.items.indexOf(page) + val position = adapter.joinedItems.indexOfFirst { it.first == page || it.second == page } if (position != -1) { val currentPosition = pager.currentItem pager.setCurrentItem(position, true) // manually call onPageChange since ViewPager listener is not triggered in this case if (currentPosition == position) { onPageChange(position) + } else { + // Call this since with double shift onPageChange wont get called (it shouldn't) + // Instead just update the page count in ui + val joinedItem = adapter.joinedItems.firstOrNull { it.first == page || it.second == page } + activity.onPageSelected( + joinedItem?.first as? ReaderPage ?: page, + joinedItem?.second != null + ) } } else { Timber.d("Page $page not found in adapter") @@ -342,6 +378,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { return true } + fun onPageSplit(currentPage: ReaderPage) { + adapter.onPageSplit(currentPage) + } + /** * Called from the containing activity when a generic motion [event] is received. It should * return true if the event was handled, false otherwise. 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 720171e4e7..718b39a444 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 @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.widget.ViewPagerAdapter import timber.log.Timber +import kotlin.math.max /** * Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted. @@ -15,14 +16,24 @@ import timber.log.Timber class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { /** - * List of currently set items. + * Paired list of currently set items. */ - var items: List = emptyList() + var joinedItems: MutableList> = mutableListOf() private set + /** Single list of items */ + private var subItems: MutableList = mutableListOf() + var nextTransition: ChapterTransition.Next? = null private set + /** Page used to start the shifted pages */ + var pageToShift: ReaderPage? = null + + /** Varibles used to check if config of the pages have changed */ + private var shifted = viewer.config.shiftDoublePage + private var doubledUp = viewer.config.doublePages + var currentChapter: ReaderChapter? = null /** @@ -38,8 +49,15 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { // We only need to add the last few pages of the previous chapter, because it'll be // selected as the current chapter when one of those pages is selected. val prevPages = chapters.prevChapter.pages + // We will take an even number of pages if the page count if even + // however we should take account full pages when deciding + val numberOfFullPages = + ( + chapters.prevChapter.pages?.count { it.fullPage || it.isolatedPage } + ?: 0 + ) if (prevPages != null) { - newItems.addAll(prevPages.takeLast(2)) + newItems.addAll(prevPages.takeLast(if ((prevPages.size + numberOfFullPages) % 2 == 0) 2 else 3)) } } @@ -75,27 +93,34 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { } } - if (viewer is R2LPagerViewer) { - newItems.reverse() - } + subItems = newItems.toMutableList() - items = newItems - notifyDataSetChanged() + var useSecondPage = false + if (shifted != viewer.config.shiftDoublePage || (doubledUp != viewer.config.doublePages && doubledUp)) { + if (shifted && (doubledUp == viewer.config.doublePages)) { + useSecondPage = true + } + shifted = viewer.config.shiftDoublePage + } + doubledUp = viewer.config.doublePages + setJoinedItems(useSecondPage) } /** * Returns the amount of items of the adapter. */ override fun getCount(): Int { - return items.size + return joinedItems.size } /** * Creates a new view for the item at the given [position]. */ override fun createView(container: ViewGroup, position: Int): View { - return when (val item = items[position]) { - is ReaderPage -> PagerPageHolder(viewer, item) + val item = joinedItems[position].first + val item2 = joinedItems[position].second + return when (item) { + is ReaderPage -> PagerPageHolder(viewer, item, item2 as? ReaderPage) is ChapterTransition -> PagerTransitionHolder(viewer, item) else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented") } @@ -106,7 +131,9 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { */ override fun getItemPosition(view: Any): Int { if (view is PositionableView) { - val position = items.indexOf(view.item) + val position = joinedItems.indexOfFirst { + view.item == (it.first to it.second) + } if (position != -1) { return position } else { @@ -115,4 +142,143 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { } return POSITION_NONE } + + fun onPageSplit(current: ReaderPage) { + val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem) + setJoinedItems( + oldCurrent?.second == current || + (current.index + 1) < ( + ( + oldCurrent?.second + ?: oldCurrent?.first + ) as? ReaderPage + )?.index ?: 0 + ) + + // The listener may be removed when we split a page, so the ui may not have updated properly + // This case usually happens when we load a new chapter and the first 2 pages need to split og + viewer.pager.post { + viewer.onPageChange(viewer.pager.currentItem) + } + } + + private fun setJoinedItems(useSecondPage: Boolean = false) { + val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem) + if (!viewer.config.doublePages) { + // If not in double mode, set up items like before + subItems.forEach { + (it as? ReaderPage)?.shiftedPage = false + } + this.joinedItems = subItems.map { Pair(it, null) }.toMutableList() + if (viewer is R2LPagerViewer) { + joinedItems.reverse() + } + } else { + val pagedItems = mutableListOf>() + val otherItems = mutableListOf() + pagedItems.add(mutableListOf()) + // Step 1: segment the pages and transition pages + subItems.forEach { + if (it is ReaderPage) { + pagedItems.last().add(it) + } else { + otherItems.add(it) + pagedItems.add(mutableListOf()) + } + } + var pagedIndex = 0 + val subJoinedItems = mutableListOf>() + // Step 2: run through each set of pages + pagedItems.forEach { items -> + + items.forEach { + it?.shiftedPage = false + } + // Step 3: If pages have been shifted, + if (viewer.config.shiftDoublePage) { + run loop@{ + var index = items.indexOf(pageToShift) + if (pageToShift?.fullPage == true) { + index = max(0, index - 1) + } + // Go from the current page and work your way back to the first page, + // or the first page that's a full page. + // This is done in case user tries to shift a page after a full page + val fullPageBeforeIndex = max( + 0, + ( + if (index > -1) ( + items.subList(0, index) + .indexOfFirst { it?.fullPage == true } + ) else -1 + ) + ) + // Add a shifted page to the first place there isnt a full page + (fullPageBeforeIndex until items.size).forEach { + if (items[it]?.fullPage == false) { + items[it]?.shiftedPage = true + return@loop + } + } + } + } + + // Step 4: Add blanks for chunking + var itemIndex = 0 + while (itemIndex < items.size) { + items[itemIndex]?.isolatedPage = false + if (items[itemIndex]?.fullPage == true || items[itemIndex]?.shiftedPage == true) { + // Add a 'blank' page after each full page. It will be used when chunked to solo a page + items.add(itemIndex + 1, null) + if (items[itemIndex]?.fullPage == true && itemIndex > 0 && + items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0 + ) { + // If a page is a full page, check if the previous page needs to be isolated + // we should check if it's an even or odd page, since even pages need shifting + // For example if Page 1 is full, Page 0 needs to be isolated + // No need to take account shifted pages, because null additions should + // always have an odd index in the list + items[itemIndex - 1]?.isolatedPage = true + items.add(itemIndex, null) + itemIndex++ + } + itemIndex++ + } + itemIndex++ + } + + // Step 5: chunk em + if (items.isNotEmpty()) { + subJoinedItems.addAll( + items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) } + ) + } + otherItems.getOrNull(pagedIndex)?.let { + subJoinedItems.add(Pair(it, null)) + pagedIndex++ + } + } + if (viewer is R2LPagerViewer) { + subJoinedItems.reverse() + } + + this.joinedItems = subJoinedItems + } + notifyDataSetChanged() + + // Step 6: Move back to our previous page or transition page + // The listener is likely off around now, but either way when shifting or doubling, + // we need to set the page back correctly + // We will however shift to the first page of the new chapter if the last page we were are + // on is not in the new chapter that has loaded + val newPage = + when { + (oldCurrent?.first as? ReaderPage)?.chapter != currentChapter && + (oldCurrent?.first as? ChapterTransition)?.from != currentChapter -> subItems.find { (it as? ReaderPage)?.chapter == currentChapter } + useSecondPage -> (oldCurrent?.second ?: oldCurrent?.first) + else -> oldCurrent?.first ?: return + } + val index = joinedItems.indexOfFirst { it.first == newPage || it.second == newPage } + viewer.pager.setCurrentItem(index, false) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index f5df42c1c6..c26738b7e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -187,7 +187,7 @@ class WebtoonViewer(val activity: ReaderActivity, val hasMargins: Boolean = fals * activity of the change and requests the preload of the next chapter if this is the last page. */ private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) { - activity.onPageSelected(page) + activity.onPageSelected(page, false) val pages = page.chapter.pages ?: return Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 82255d0a2d..ea7fd5aa8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -5,6 +5,8 @@ import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout +import eu.kanade.tachiyomi.util.lang.addBetaTag import kotlinx.coroutines.flow.launchIn import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys @@ -186,6 +188,21 @@ class SettingsReaderController : SettingsController() { titleRes = R.string.crop_borders defaultValue = false } + intListPreference(activity) { + key = Keys.pageLayout + title = context.getString(R.string.page_layout).addBetaTag(context) + dialogTitleRes = R.string.page_layout + entriesRes = arrayOf( + R.string.single_page, + R.string.double_pages, + R.string.automatic_orientation + ) + entryRange = 0..2 + defaultValue = 2 + } + infoPreference(R.string.automatic_can_still_switch).apply { + preferences.pageLayout().asImmediateFlow { isVisible = it == PageLayout.AUTOMATIC }.launchIn(viewScope) + } } preferenceCategory { titleRes = R.string.webtoon diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index ad39ed63a3..d51b35dee6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -1,11 +1,19 @@ package eu.kanade.tachiyomi.util.lang +import android.content.Context +import android.graphics.Typeface import android.text.Spannable import android.text.SpannableString +import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.BackgroundColorSpan import android.text.style.ForegroundColorSpan +import android.text.style.RelativeSizeSpan +import android.text.style.StyleSpan +import android.text.style.SuperscriptSpan import androidx.annotation.ColorInt +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.getResourceColor import kotlin.math.floor /** @@ -90,3 +98,13 @@ fun String.indexesOf(substr: String, ignoreCase: Boolean = true): List { } } } + +fun String.addBetaTag(context: Context): Spanned { + val betaText = context.getString(R.string.beta) + val betaSpan = SpannableStringBuilder(this + betaText) + betaSpan.setSpan(SuperscriptSpan(), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + betaSpan.setSpan(RelativeSizeSpan(0.75f), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + betaSpan.setSpan(StyleSpan(Typeface.BOLD), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + betaSpan.setSpan(ForegroundColorSpan(context.getResourceColor(R.attr.colorAccent)), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return betaSpan +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt index c019f99898..295fb88e5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt @@ -17,6 +17,9 @@ fun launchNow(block: suspend CoroutineScope.() -> Unit): Job = fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.IO, block = block) +fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job = + launch(Dispatchers.Main, block = block) + suspend fun withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block) suspend fun withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt index 2441bada85..b8c79aed6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt @@ -29,6 +29,13 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att private var pref: Preference? = null private var prefOffset = 0 private var popup: PopupMenu? = null + var title: CharSequence + get() { + return binding.titleView.text + } + set(value) { + binding.titleView.text = value + } var onItemSelectedListener: ((Int) -> Unit)? = null set(value) { @@ -49,7 +56,7 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att val a = context.obtainStyledAttributes(attrs, R.styleable.ReaderSpinnerView, 0, 0) val str = a.getString(R.styleable.ReaderSpinnerView_title) ?: "" - binding.titleView.text = str + title = str val entries = (a.getTextArray(R.styleable.ReaderSpinnerView_android_entries) ?: emptyArray()).map { it.toString() } this.entries = entries diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt index 0b0ac6ffdd..f47d2a3c55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MatPreference.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.widget.preference import android.app.Activity import android.content.Context import android.util.AttributeSet +import androidx.annotation.StringRes import androidx.preference.Preference import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss @@ -23,6 +24,8 @@ open class MatPreference @JvmOverloads constructor( private var isShowing = false var customSummary: String? = null + @StringRes var dialogTitleRes: Int? = null + override fun onClick() { if (!isShowing) { dialog().apply { @@ -38,7 +41,9 @@ open class MatPreference @JvmOverloads constructor( open fun dialog(): MaterialDialog { return MaterialDialog(activity ?: context).apply { - if (title != null) { + if (dialogTitleRes != null) { + title(res = dialogTitleRes) + } else if (title != null) { title(text = title.toString()) } negativeButton(android.R.string.cancel) diff --git a/app/src/main/res/drawable/ic_book_open_variant_24dp.xml b/app/src/main/res/drawable/ic_book_open_variant_24dp.xml new file mode 100644 index 0000000000..f463d84c28 --- /dev/null +++ b/app/src/main/res/drawable/ic_book_open_variant_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outline_photo_24dp.xml b/app/src/main/res/drawable/ic_outline_photo_24dp.xml new file mode 100644 index 0000000000..f2524d59ed --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_photo_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_save_24dp.xml b/app/src/main/res/drawable/ic_outline_save_24dp.xml new file mode 100644 index 0000000000..dbc1b91f4d --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_save_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_share_24dp.xml b/app/src/main/res/drawable/ic_outline_share_24dp.xml new file mode 100644 index 0000000000..4cd6541066 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_share_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_page_next_outline_24dp.xml b/app/src/main/res/drawable/ic_page_next_outline_24dp.xml new file mode 100644 index 0000000000..c9afa7ced6 --- /dev/null +++ b/app/src/main/res/drawable/ic_page_next_outline_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_page_previous_outline_24dp.xml b/app/src/main/res/drawable/ic_page_previous_outline_24dp.xml new file mode 100644 index 0000000000..ef908121b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_page_previous_outline_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_single_page_24dp.xml b/app/src/main/res/drawable/ic_single_page_24dp.xml new file mode 100644 index 0000000000..c41f46a0f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_single_page_24dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/reader_toolbar_ripple.xml b/app/src/main/res/drawable/reader_toolbar_ripple.xml index 01a7364f8c..407c8ed029 100644 --- a/app/src/main/res/drawable/reader_toolbar_ripple.xml +++ b/app/src/main/res/drawable/reader_toolbar_ripple.xml @@ -1,4 +1,5 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rect_ripple.xml b/app/src/main/res/drawable/rect_ripple.xml new file mode 100644 index 0000000000..0b72b6ee74 --- /dev/null +++ b/app/src/main/res/drawable/rect_ripple.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/reader_chapters_sheet.xml b/app/src/main/res/layout/reader_chapters_sheet.xml index 144442f9ae..70f05bf378 100644 --- a/app/src/main/res/layout/reader_chapters_sheet.xml +++ b/app/src/main/res/layout/reader_chapters_sheet.xml @@ -59,10 +59,26 @@ app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/chapters_button" - app:layout_constraintEnd_toStartOf="@id/display_options" + app:layout_constraintEnd_toStartOf="@id/double_page" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_open_in_webview_24dp" /> + + diff --git a/app/src/main/res/layout/reader_nav.xml b/app/src/main/res/layout/reader_nav.xml index da0adae427..5c6563db99 100644 --- a/app/src/main/res/layout/reader_nav.xml +++ b/app/src/main/res/layout/reader_nav.xml @@ -46,12 +46,12 @@ + tools:text="12-14" /> + + - + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a977265777..bd46c8f603 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -70,6 +70,12 @@ @string/right_and_left_nav + + @string/single_page + @string/double_pages + @string/automatic + + @string/none @string/horizontally diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e88a9aa0e8..46f2cb0362 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,6 +165,7 @@ Hide category hopper Hide category hopper on scroll More library settings + Shift one page over New chapters found @@ -279,9 +280,6 @@ Custom filter Set as cover - Set page as cover - Share page - Save page %1$s details Reader settings Set as default for all @@ -306,6 +304,14 @@ Next chapter Previous chapter + Set first page as cover + Share first page + Save first page + + Set second page as cover + Share second page + Save second page + Fullscreen Animate page transitions @@ -352,6 +358,10 @@ Original size Smart fit Zoom start position + Double pages + Single page + Switch to double pages + Switch to single page Force portrait Force landscape R @@ -382,6 +392,9 @@ Pad cutout areas Start past cutout Ignore cutout areas + Page layout + While using automatic page layout, you can still switch between layouts while reading without overriding this setting + Automatic (based on orientation) About this %1$s @@ -783,6 +796,7 @@ Auto Automatic Back + BETA Bottom Report a Bug Cancel