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