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"
This commit is contained in:
Jays2Kings 2021-04-05 14:55:22 -04:00 committed by GitHub
parent 4e4f9b339f
commit 6d6766a86a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 948 additions and 115 deletions

View File

@ -65,6 +65,8 @@ object PreferenceKeys {
const val webtoonNavInverted = "reader_tapping_inverted_webtoon" const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
const val pageLayout = "page_layout"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user" const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayNewUserWebtoon = "reader_navigation_overlay_new_user_webtoon" const val showNavigationOverlayNewUserWebtoon = "reader_navigation_overlay_new_user_webtoon"

View File

@ -11,6 +11,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation 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.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import java.io.File import java.io.File
@ -143,6 +144,8 @@ class PreferencesHelper(val context: Context) {
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, ViewerNavigation.TappingInvertMode.NONE) 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 showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun showNavigationOverlayNewUserWebtoon() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUserWebtoon, true) fun showNavigationOverlayNewUserWebtoon() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUserWebtoon, true)

View File

@ -8,10 +8,12 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.LayerDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -20,9 +22,10 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.GestureDetectorCompat
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomsheet.BottomSheetBehavior 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.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer 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.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.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer 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.getResourceColor
import eu.kanade.tachiyomi.util.system.hasSideNavBar import eu.kanade.tachiyomi.util.system.hasSideNavBar
import eu.kanade.tachiyomi.util.system.isBottomTappable 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.launchIO
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@ -160,6 +166,10 @@ class ReaderActivity :
var isLoading = false var isLoading = false
var lastShiftDoubleState: Boolean? = null
var indexPageToShift: Int? = null
var indexChapterToShift: Long? = null
companion object { companion object {
@Suppress("unused") @Suppress("unused")
const val LEFT_TO_RIGHT = 1 const val LEFT_TO_RIGHT = 1
@ -168,6 +178,10 @@ class ReaderActivity :
const val WEBTOON = 4 const val WEBTOON = 4
const val VERTICAL_PLUS = 5 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 { fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
val intent = Intent(context, ReaderActivity::class.java) val intent = Intent(context, ReaderActivity::class.java)
intent.putExtra("manga", manga.id) intent.putExtra("manga", manga.id)
@ -217,6 +231,9 @@ class ReaderActivity :
if (savedInstanceState != null) { if (savedInstanceState != null) {
menuVisible = savedInstanceState.getBoolean(::menuVisible.name) 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 binding.readerNav.root.isVisible = menuVisible
} else { } else {
binding.readerNav.root.gone() binding.readerNav.root.gone()
@ -251,6 +268,16 @@ class ReaderActivity :
*/ */
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(::menuVisible.name, menuVisible) 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) { if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange() presenter.onSaveInstanceStateNonConfigurationChange()
} }
@ -279,6 +306,65 @@ class ReaderActivity :
return true 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() { private fun popToMain() {
presenter.onBackPressed() presenter.onBackPressed()
if (fromUrl) { 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 { binding.readerNav.leftChapter.setOnClickListener {
if (isLoading) { if (isLoading) {
return@setOnClickListener return@setOnClickListener
@ -579,6 +675,7 @@ class ReaderActivity :
binding.viewerContainer.removeAllViews() binding.viewerContainer.removeAllViews()
} }
viewer = newViewer viewer = newViewer
binding.chaptersSheet.doublePage.isVisible = viewer is PagerViewer
binding.viewerContainer.addView(newViewer.getView()) binding.viewerContainer.addView(newViewer.getView())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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.navigationOverlay.isLTR = !(viewer is L2RPagerViewer)
binding.viewerContainer.setBackgroundColor( binding.viewerContainer.setBackgroundColor(
if (viewer is WebtoonViewer) { if (viewer is WebtoonViewer) {
@ -606,6 +711,7 @@ class ReaderActivity :
binding.pleaseWait.visible() binding.pleaseWait.visible()
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long)) binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
invalidateOptionsMenu()
} }
override fun onPause() { override fun onPause() {
@ -613,12 +719,44 @@ class ReaderActivity :
super.onPause() 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 * 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. * method to the current viewer, but also set the subtitle on the binding.toolbar.
*/ */
fun setChapters(viewerChapters: ViewerChapters) { fun setChapters(viewerChapters: ViewerChapters) {
binding.pleaseWait.gone() 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) viewer?.setChapters(viewerChapters)
intentPageNumber?.let { moveToPageIndex(it) } intentPageNumber?.let { moveToPageIndex(it) }
intentPageNumber = null intentPageNumber = null
@ -687,58 +825,100 @@ class ReaderActivity :
* bottom menu and delegates the change to the presenter. * bottom menu and delegates the change to the presenter.
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage) { fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
presenter.onPageSelected(page) presenter.onPageSelected(page, hasExtraPage)
val pages = page.chapter.pages ?: return val pages = page.chapter.pages ?: return
val currentPage = page.number val currentPage = if (hasExtraPage) {
val totalPages = pages.size if (resources.isLTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}"
// 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()
} else { } else {
binding.readerNav.leftPageText.text = currentPage.toString() "${page.number}"
binding.readerNav.rightPageText.text = totalPages.toString() }
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) { if (binding.chaptersSheet.chaptersBottomSheet.selectedChapterId != page.chapter.chapter.id) {
binding.chaptersSheet.chaptersBottomSheet.refreshList() binding.chaptersSheet.chaptersBottomSheet.refreshList()
} }
// Set seekbar progress // Set seekbar progress
binding.readerNav.pageSeekbar.max = pages.lastIndex 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 * Called from the viewer whenever a [page] is long clicked. A bottom sheet with a list of
* actions to perform is shown. * actions to perform is shown.
*/ */
fun onPageLongTap(page: ReaderPage) { fun onPageLongTap(page: ReaderPage, extraPage: ReaderPage? = null) {
val items = listOf( val items = if (extraPage != null) {
MaterialMenuSheet.MenuSheetItem( listOf(
0, MaterialMenuSheet.MenuSheetItem(
R.drawable.ic_share_24dp, 3,
R.string.share R.drawable.ic_outline_share_24dp,
), R.string.share_second_page
MaterialMenuSheet.MenuSheetItem( ),
1, MaterialMenuSheet.MenuSheetItem(
R.drawable.ic_save_24dp, 4,
R.string.save R.drawable.ic_outline_save_24dp,
), R.string.save_second_page
MaterialMenuSheet.MenuSheetItem( ),
2, MaterialMenuSheet.MenuSheetItem(
R.drawable.ic_photo_24dp, 5,
R.string.set_as_cover 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 -> MaterialMenuSheet(this, items) { _, item ->
when (item) { when (item) {
0 -> shareImage(page) 0 -> shareImage(page)
1 -> saveImage(page) 1 -> saveImage(page)
2 -> showSetCoverPrompt(page) 2 -> showSetCoverPrompt(page)
3 -> extraPage?.let { shareImage(it) }
4 -> extraPage?.let { saveImage(it) }
5 -> extraPage?.let { showSetCoverPrompt(it) }
} }
true true
}.show() }.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 { private fun handleIntentAction(intent: Intent): Boolean {
val uri = intent.data ?: return false val uri = intent.data ?: return false
if (!presenter.canLoadUrl(uri)) { if (!presenter.canLoadUrl(uri)) {
@ -1000,6 +1185,12 @@ class ReaderActivity :
preferences.alwaysShowChapterTransition().asFlow() preferences.alwaysShowChapterTransition().asFlow()
.onEach { showNewChapter = it } .onEach { showNewChapter = it }
.launchIn(scope) .launchIn(scope)
preferences.pageLayout().asFlow()
.onEach {
setBottomNavButtons(it)
}
.launchIn(scope)
} }
/** /**

View File

@ -86,6 +86,9 @@ class ReaderPresenter(
*/ */
private val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>() private val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
val viewerChapters: ViewerChapters?
get() = viewerChaptersRelay.value
/** /**
* Relay used when loading prev/next chapter needed to lock the UI (with a dialog). * 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) } .doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
.subscribeFirst( .subscribeFirst(
{ view, _ -> { view, _ ->
val lastPage = if (chapter.pages_left <= 1) 0 else chapter.last_page_read view.moveToPageIndex(0)
view.moveToPageIndex(lastPage)
view.refreshChapters() view.refreshChapters()
}, },
{ _, _ -> { _, _ ->
@ -458,7 +460,7 @@ class ReaderPresenter(
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
* [page]'s chapter is different from the currently active. * [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 currentChapters = viewerChaptersRelay.value ?: return
val selectedChapter = page.chapter val selectedChapter = page.chapter
@ -467,7 +469,10 @@ class ReaderPresenter(
selectedChapter.chapter.last_page_read = page.index selectedChapter.chapter.last_page_read = page.index
selectedChapter.chapter.pages_left = selectedChapter.chapter.pages_left =
(selectedChapter.pages?.size ?: page.index) - page.index (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 selectedChapter.chapter.read = true
updateTrackChapterRead(selectedChapter) updateTrackChapterRead(selectedChapter)
deleteChapterIfNeeded(selectedChapter) deleteChapterIfNeeded(selectedChapter)

View File

@ -17,8 +17,9 @@ sealed class ChapterTransition {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is ChapterTransition) return false if (other !is ChapterTransition) return false
if (from == other.from && to == other.to) return true if (from == other.from && to == other.to && to != null) return true
if (from == other.to && to == other.from) 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 return false
} }

View File

@ -10,8 +10,19 @@ class ReaderPage(
imageUrl: String? = null, imageUrl: String? = null,
var stream: (() -> InputStream)? = null, var stream: (() -> InputStream)? = null,
var bg: Drawable? = 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) { ) : Page(index, url, imageUrl, null) {
lateinit var chapter: ReaderChapter 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
}
} }

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.bindToPreference import eu.kanade.tachiyomi.util.bindToPreference
import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import eu.kanade.tachiyomi.widget.BaseReaderSettingsView import eu.kanade.tachiyomi.widget.BaseReaderSettingsView
@ -27,12 +28,18 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
binding.pagerNav.bindToPreference(preferences.navigationModePager()) binding.pagerNav.bindToPreference(preferences.navigationModePager())
binding.pagerInvert.bindToPreference(preferences.pagerNavInverted()) binding.pagerInvert.bindToPreference(preferences.pagerNavInverted())
binding.extendPastCutout.bindToPreference(preferences.pagerCutoutBehavior()) 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 mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaViewer() ?: 0
val isWebtoonView = mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS val isWebtoonView = mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS
val hasMargins = mangaViewer == ReaderActivity.VERTICAL_PLUS val hasMargins = mangaViewer == ReaderActivity.VERTICAL_PLUS
binding.cropBordersWebtoon.bindToPreference(if (hasMargins) preferences.cropBorders() else preferences.cropBordersWebtoon()) 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.webtoonEnableZoomOut.bindToPreference(preferences.webtoonEnableZoomOut())
binding.webtoonNav.bindToPreference(preferences.navigationModeWebtoon()) binding.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
binding.webtoonInvert.bindToPreference(preferences.webtoonNavInverted()) binding.webtoonInvert.bindToPreference(preferences.webtoonNavInverted())
@ -49,8 +56,22 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
} }
private fun updatePagedGroup(show: Boolean) { private fun updatePagedGroup(show: Boolean) {
listOf(binding.scaleType, binding.zoomStart, binding.cropBorders, binding.pageTransitions, binding.pagerNav, binding.pagerInvert).forEach { it.visibleIf(show) } listOf(
listOf(binding.cropBordersWebtoon, binding.webtoonSidePadding, binding.webtoonEnableZoomOut, binding.webtoonNav, binding.webtoonInvert).forEach { it.visibleIf(!show) } 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()) { val isFullFit = when (preferences.imageScaleType().get()) {
SubsamplingScaleImageView.SCALE_TYPE_FIT_HEIGHT, SubsamplingScaleImageView.SCALE_TYPE_FIT_HEIGHT,
SubsamplingScaleImageView.SCALE_TYPE_SMART_FIT, SubsamplingScaleImageView.SCALE_TYPE_SMART_FIT,

View File

@ -16,6 +16,7 @@ abstract class ViewerConfig(preferences: PreferencesHelper) {
protected val scope = CoroutineScope(Job() + Dispatchers.Main) protected val scope = CoroutineScope(Job() + Dispatchers.Main)
var imagePropertyChangedListener: (() -> Unit)? = null var imagePropertyChangedListener: (() -> Unit)? = null
var reloadChapterListener: ((Boolean) -> Unit)? = null
var navigationModeChangedListener: (() -> Unit)? = null var navigationModeChangedListener: (() -> Unit)? = null
var navigationModeInvertedListener: (() -> Unit)? = null var navigationModeInvertedListener: (() -> Unit)? = null

View File

@ -38,6 +38,18 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
var cutoutBehavior = 0 var cutoutBehavior = 0
private set 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 { init {
preferences.pageTransitions() preferences.pageTransitions()
.register({ usePageTransitions = it }) .register({ usePageTransitions = it })
@ -75,6 +87,25 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
preferences.readerTheme() preferences.readerTheme()
.register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() }) .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() navigationOverlayForNewUser = preferences.showNavigationOverlayNewUser().get()
if (navigationOverlayForNewUser) { if (navigationOverlayForNewUser) {
preferences.showNavigationOverlayNewUser().set(false) preferences.showNavigationOverlayNewUser().set(false)
@ -141,3 +172,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
const val CUTOUT_IGNORE = 2 const val CUTOUT_IGNORE = 2
} }
} }
object PageLayout {
const val SINGLE_PAGE = 0
const val DOUBLE_PAGES = 1
const val AUTOMATIC = 2
}

View File

@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.GestureDetector import android.view.GestureDetector
import android.view.Gravity import android.view.Gravity
@ -48,8 +51,11 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.max
/** /**
* View of the ViewPager that contains a page of a chapter. * View of the ViewPager that contains a page of a chapter.
@ -57,14 +63,15 @@ import java.util.concurrent.TimeUnit
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class PagerPageHolder( class PagerPageHolder(
val viewer: PagerViewer, val viewer: PagerViewer,
val page: ReaderPage val page: ReaderPage,
private var extraPage: ReaderPage? = null
) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView { ) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView {
/** /**
* Item that identifies this view. Needed by the adapter to not recreate views. * Item that identifies this view. Needed by the adapter to not recreate views.
*/ */
override val item override val item
get() = page get() = page to extraPage
/** /**
* Loading progress bar to indicate the current progress. * Loading progress bar to indicate the current progress.
@ -101,12 +108,28 @@ class PagerPageHolder(
*/ */
private var progressSubscription: Subscription? = null 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 * 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). * the appropiate image view depending if the image is animated (GIF).
*/ */
private var readImageHeaderSubscription: Subscription? = null 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 { init {
addView(progressBar) addView(progressBar)
observeStatus() observeStatus()
@ -124,8 +147,10 @@ class PagerPageHolder(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
unsubscribeProgress() unsubscribeProgress(1)
unsubscribeStatus() unsubscribeStatus(1)
unsubscribeProgress(2)
unsubscribeStatus(2)
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null) subsamplingImageView?.setOnImageEventListener(null)
} }
@ -141,7 +166,18 @@ class PagerPageHolder(
val loader = page.chapter.pageLoader ?: return val loader = page.chapter.pageLoader ?: return
statusSubscription = loader.getPage(page) statusSubscription = loader.getPage(page)
.observeOn(AndroidSchedulers.mainThread()) .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() .distinctUntilChanged()
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .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() setDownloading()
} }
Page.READY -> { Page.READY -> {
setImage() if (extraStatus == Page.READY || extraPage == null) {
unsubscribeProgress() setImage()
}
unsubscribeProgress(1)
} }
Page.ERROR -> { Page.ERROR -> {
setError() 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. * Unsubscribes from the status subscription.
*/ */
private fun unsubscribeStatus() { private fun unsubscribeStatus(page: Int) {
statusSubscription?.unsubscribe() val subscription = if (page == 1) statusSubscription else extraStatusSubscription
statusSubscription = null subscription?.unsubscribe()
if (page == 1) statusSubscription = null else extraStatusSubscription = null
} }
/** /**
* Unsubscribes from the progress subscription. * Unsubscribes from the progress subscription.
*/ */
private fun unsubscribeProgress() { private fun unsubscribeProgress(page: Int) {
progressSubscription?.unsubscribe() val subscription = if (page == 1) progressSubscription else extraProgressSubscription
progressSubscription = null subscription?.unsubscribe()
if (page == 1) progressSubscription = null else extraProgressSubscription = null
} }
/** /**
@ -244,23 +331,30 @@ class PagerPageHolder(
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
val streamFn = page.stream ?: return val streamFn = page.stream ?: return
val streamFn2 = extraPage?.stream
var openStream: InputStream? = null var openStream: InputStream? = null
readImageHeaderSubscription = Observable readImageHeaderSubscription = Observable
.fromCallable { .fromCallable {
val stream = streamFn().buffered(16) 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()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (skipExtra) {
onPageSplit()
}
if (!isAnimated) { if (!isAnimated) {
if (viewer.config.readerTheme >= 2) { if (viewer.config.readerTheme >= 2) {
val imageView = initSubsamplingImageView() val imageView = initSubsamplingImageView()
if (page.bg != null && 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.setImage(ImageSource.inputStream(openStream!!))
imageView.background = page.bg imageView.background = page.bg
@ -275,7 +369,7 @@ class PagerPageHolder(
launchUI { launchUI {
imageView.background = setBG(bytesArray) imageView.background = setBG(bytesArray)
page.bg = imageView.background page.bg = imageView.background
page.bgType = getBGType(viewer.config.readerTheme, context) page.bgType = getBGType(viewer.config.readerTheme, context) + item.hashCode()
} }
} }
} else { } else {
@ -293,7 +387,9 @@ class PagerPageHolder(
// Keep the Rx stream alive to close the input stream only when unsubscribed // Keep the Rx stream alive to close the input stream only when unsubscribed
.flatMap { Observable.never<Unit>() } .flatMap { Observable.never<Unit>() }
.doOnUnsubscribe { .doOnUnsubscribe {
try { openStream?.close() } catch (e: Exception) {} try {
openStream?.close()
} catch (e: Exception) {}
} }
.subscribe({}, {}) .subscribe({}, {})
} }
@ -468,6 +564,9 @@ class PagerPageHolder(
setText(R.string.retry) setText(R.string.retry)
setOnClickListener { setOnClickListener {
page.chapter.pageLoader?.retryPage(page) page.chapter.pageLoader?.retryPage(page)
extraPage?.let {
it.chapter.pageLoader?.retryPage(it)
}
} }
} }
addView(retryButton) addView(retryButton)
@ -530,6 +629,83 @@ class PagerPageHolder(
return decodeLayout 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. * 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 { companion object {
fun getBGType(readerTheme: Int, context: Context): Int { fun getBGType(readerTheme: Int, context: Context): Int {
return if (readerTheme == 3) { return if (readerTheme == 3) {

View File

@ -60,29 +60,29 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
field = value field = value
if (value) { if (value) {
awaitingIdleViewerChapters?.let { awaitingIdleViewerChapters?.let {
setChaptersInternal(it) setChaptersDoubleShift(it)
awaitingIdleViewerChapters = null 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 { init {
pager.gone() // Don't layout the pager yet pager.gone() // Don't layout the pager yet
pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
pager.offscreenPageLimit = 1 pager.offscreenPageLimit = 1
pager.id = R.id.reader_pager pager.id = R.id.reader_pager
pager.adapter = adapter pager.adapter = adapter
pager.addOnPageChangeListener( pager.addOnPageChangeListener(pagerListener)
object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
onPageChange(position)
}
override fun onPageScrollStateChanged(state: Int) {
isIdle = state == ViewPager.SCROLL_STATE_IDLE
}
}
)
pager.tapListener = f@{ event -> pager.tapListener = f@{ event ->
if (!config.tappingEnabled) { if (!config.tappingEnabled) {
activity.toggleMenu() activity.toggleMenu()
@ -101,9 +101,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
pager.longTapListener = f@{ pager.longTapListener = f@{
if (activity.menuVisible || config.longTapEnabled) { if (activity.menuVisible || config.longTapEnabled) {
val item = adapter.items.getOrNull(pager.currentItem) val item = adapter.joinedItems.getOrNull(pager.currentItem)
if (item is ReaderPage) { val firstPage = item?.first as? ReaderPage
activity.onPageLongTap(item) val secondPage = item?.second as? ReaderPage
if (firstPage is ReaderPage) {
activity.onPageLongTap(firstPage, secondPage)
return@f true return@f true
} }
} }
@ -114,6 +116,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
refreshAdapter() refreshAdapter()
} }
config.reloadChapterListener = {
activity.reloadChapters(it)
}
config.navigationModeChangedListener = { config.navigationModeChangedListener = {
val showOnStart = config.navigationOverlayForNewUser val showOnStart = config.navigationOverlayForNewUser
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) 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 * Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
*/ */
private fun onPageChange(position: Int) { fun onPageChange(position: Int) {
val page = adapter.items.getOrNull(position) val page = adapter.joinedItems.getOrNull(position)
if (page != null && currentPage != page) { if (page != null && currentPage != page) {
val allowPreload = checkAllowPreload(page as? ReaderPage) val allowPreload = checkAllowPreload(page.first as? ReaderPage)
currentPage = page currentPage = page.first
when (page) { when (val aPage = page.first) {
is ReaderPage -> onReaderPageSelected(page, allowPreload) is ReaderPage -> onReaderPageSelected(aPage, allowPreload, page.second != null)
is ChapterTransition -> onTransitionSelected(page) 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 * 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. * 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) { private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, hasExtraPage: Boolean) {
activity.onPageSelected(page) activity.onPageSelected(page, hasExtraPage)
val offset = if (hasExtraPage) 1 else 0
val pages = page.chapter.pages ?: return 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 // Preload next chapter once we're within the last 5 pages of the current chapter
val inPreloadRange = pages.size - page.number < 5 val inPreloadRange = pages.size - page.number < 5
if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) { 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, * 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. * it sets the chapters immediately, otherwise they are saved and set when it becomes idle.
*/ */
override fun setChapters(chapters: ViewerChapters) { override fun setChapters(chapters: ViewerChapters) {
if (isIdle) { if (isIdle) {
setChaptersInternal(chapters) setChaptersDoubleShift(chapters)
} else { } else {
awaitingIdleViewerChapters = chapters awaitingIdleViewerChapters = chapters
} }
@ -219,7 +246,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
*/ */
private fun setChaptersInternal(chapters: ViewerChapters) { private fun setChaptersInternal(chapters: ViewerChapters) {
Timber.d("setChaptersInternal") Timber.d("setChaptersInternal")
val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull( val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull(
pager pager
.currentItem .currentItem
) is ChapterTransition ) is ChapterTransition
@ -232,6 +259,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
moveToPage(pages[chapters.currChapter.requestedPage]) moveToPage(pages[chapters.currChapter.requestedPage])
pager.visible() pager.visible()
} }
activity.invalidateOptionsMenu()
} }
/** /**
@ -239,13 +267,21 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
*/ */
override fun moveToPage(page: ReaderPage) { override fun moveToPage(page: ReaderPage) {
Timber.d("moveToPage ${page.number}") 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) { if (position != -1) {
val currentPosition = pager.currentItem val currentPosition = pager.currentItem
pager.setCurrentItem(position, true) pager.setCurrentItem(position, true)
// manually call onPageChange since ViewPager listener is not triggered in this case // manually call onPageChange since ViewPager listener is not triggered in this case
if (currentPosition == position) { if (currentPosition == position) {
onPageChange(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 { } else {
Timber.d("Page $page not found in adapter") Timber.d("Page $page not found in adapter")
@ -342,6 +378,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
return true return true
} }
fun onPageSplit(currentPage: ReaderPage) {
adapter.onPageSplit(currentPage)
}
/** /**
* Called from the containing activity when a generic motion [event] is received. It should * Called from the containing activity when a generic motion [event] is received. It should
* return true if the event was handled, false otherwise. * return true if the event was handled, false otherwise.

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.widget.ViewPagerAdapter import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import timber.log.Timber import timber.log.Timber
import kotlin.math.max
/** /**
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted. * 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() { class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/** /**
* List of currently set items. * Paired list of currently set items.
*/ */
var items: List<Any> = emptyList() var joinedItems: MutableList<Pair<Any, Any?>> = mutableListOf()
private set private set
/** Single list of items */
private var subItems: MutableList<Any> = mutableListOf()
var nextTransition: ChapterTransition.Next? = null var nextTransition: ChapterTransition.Next? = null
private set 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 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 // 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. // selected as the current chapter when one of those pages is selected.
val prevPages = chapters.prevChapter.pages 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) { 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) { subItems = newItems.toMutableList()
newItems.reverse()
}
items = newItems var useSecondPage = false
notifyDataSetChanged() 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. * Returns the amount of items of the adapter.
*/ */
override fun getCount(): Int { override fun getCount(): Int {
return items.size return joinedItems.size
} }
/** /**
* Creates a new view for the item at the given [position]. * Creates a new view for the item at the given [position].
*/ */
override fun createView(container: ViewGroup, position: Int): View { override fun createView(container: ViewGroup, position: Int): View {
return when (val item = items[position]) { val item = joinedItems[position].first
is ReaderPage -> PagerPageHolder(viewer, item) val item2 = joinedItems[position].second
return when (item) {
is ReaderPage -> PagerPageHolder(viewer, item, item2 as? ReaderPage)
is ChapterTransition -> PagerTransitionHolder(viewer, item) is ChapterTransition -> PagerTransitionHolder(viewer, item)
else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented") 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 { override fun getItemPosition(view: Any): Int {
if (view is PositionableView) { if (view is PositionableView) {
val position = items.indexOf(view.item) val position = joinedItems.indexOfFirst {
view.item == (it.first to it.second)
}
if (position != -1) { if (position != -1) {
return position return position
} else { } else {
@ -115,4 +142,143 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
} }
return POSITION_NONE 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<Any, Any?>(it, null) }.toMutableList()
if (viewer is R2LPagerViewer) {
joinedItems.reverse()
}
} else {
val pagedItems = mutableListOf<MutableList<ReaderPage?>>()
val otherItems = mutableListOf<Any>()
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<Pair<Any, Any?>>()
// 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)
}
} }

View File

@ -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. * 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) { private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
activity.onPageSelected(page) activity.onPageSelected(page, false)
val pages = page.chapter.pages ?: return val pages = page.chapter.pages ?: return
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}") Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")

View File

@ -5,6 +5,8 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation 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 kotlinx.coroutines.flow.launchIn
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@ -186,6 +188,21 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.crop_borders titleRes = R.string.crop_borders
defaultValue = false 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 { preferenceCategory {
titleRes = R.string.webtoon titleRes = R.string.webtoon

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi.util.lang package eu.kanade.tachiyomi.util.lang
import android.content.Context
import android.graphics.Typeface
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.BackgroundColorSpan import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan 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 androidx.annotation.ColorInt
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlin.math.floor import kotlin.math.floor
/** /**
@ -90,3 +98,13 @@ fun String.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
} }
} }
} }
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
}

View File

@ -17,6 +17,9 @@ fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.IO, block = block) launch(Dispatchers.IO, block = block)
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.Main, block = block)
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block) suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block) suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)

View File

@ -29,6 +29,13 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
private var pref: Preference<Int>? = null private var pref: Preference<Int>? = null
private var prefOffset = 0 private var prefOffset = 0
private var popup: PopupMenu? = null private var popup: PopupMenu? = null
var title: CharSequence
get() {
return binding.titleView.text
}
set(value) {
binding.titleView.text = value
}
var onItemSelectedListener: ((Int) -> Unit)? = null var onItemSelectedListener: ((Int) -> Unit)? = null
set(value) { 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 a = context.obtainStyledAttributes(attrs, R.styleable.ReaderSpinnerView, 0, 0)
val str = a.getString(R.styleable.ReaderSpinnerView_title) ?: "" 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() } val entries = (a.getTextArray(R.styleable.ReaderSpinnerView_android_entries) ?: emptyArray()).map { it.toString() }
this.entries = entries this.entries = entries

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.widget.preference
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.StringRes
import androidx.preference.Preference import androidx.preference.Preference
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.callbacks.onDismiss
@ -23,6 +24,8 @@ open class MatPreference @JvmOverloads constructor(
private var isShowing = false private var isShowing = false
var customSummary: String? = null var customSummary: String? = null
@StringRes var dialogTitleRes: Int? = null
override fun onClick() { override fun onClick() {
if (!isShowing) { if (!isShowing) {
dialog().apply { dialog().apply {
@ -38,7 +41,9 @@ open class MatPreference @JvmOverloads constructor(
open fun dialog(): MaterialDialog { open fun dialog(): MaterialDialog {
return MaterialDialog(activity ?: context).apply { return MaterialDialog(activity ?: context).apply {
if (title != null) { if (dialogTitleRes != null) {
title(res = dialogTitleRes)
} else if (title != null) {
title(text = title.toString()) title(text = title.toString())
} }
negativeButton(android.R.string.cancel) negativeButton(android.R.string.cancel)

View File

@ -0,0 +1,9 @@
<!-- drawable/book_open_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path android:fillColor="#000" android:pathData="M17.5 14.33C18.29 14.33 19.13 14.41 20 14.57V16.07C19.38 15.91 18.54 15.83 17.5 15.83C15.6 15.83 14.11 16.16 13 16.82V15.13C14.17 14.6 15.67 14.33 17.5 14.33M13 12.46C14.29 11.93 15.79 11.67 17.5 11.67C18.29 11.67 19.13 11.74 20 11.9V13.4C19.38 13.24 18.54 13.16 17.5 13.16C15.6 13.16 14.11 13.5 13 14.15M17.5 10.5C15.6 10.5 14.11 10.82 13 11.5V9.84C14.23 9.28 15.73 9 17.5 9C18.29 9 19.13 9.08 20 9.23V10.78C19.26 10.59 18.41 10.5 17.5 10.5M21 18.5V7C19.96 6.67 18.79 6.5 17.5 6.5C15.45 6.5 13.62 7 12 8V19.5C13.62 18.5 15.45 18 17.5 18C18.69 18 19.86 18.16 21 18.5M17.5 4.5C19.85 4.5 21.69 5 23 6V20.56C23 20.68 22.95 20.8 22.84 20.91C22.73 21 22.61 21.08 22.5 21.08C22.39 21.08 22.31 21.06 22.25 21.03C20.97 20.34 19.38 20 17.5 20C15.45 20 13.62 20.5 12 21.5C10.66 20.5 8.83 20 6.5 20C4.84 20 3.25 20.36 1.75 21.07C1.72 21.08 1.68 21.08 1.63 21.1C1.59 21.11 1.55 21.12 1.5 21.12C1.39 21.12 1.27 21.08 1.16 21C1.05 20.89 1 20.78 1 20.65V6C2.34 5 4.18 4.5 6.5 4.5C8.83 4.5 10.66 5 12 6C13.34 5 15.17 4.5 17.5 4.5Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

View File

@ -0,0 +1,9 @@
<!-- drawable/page_next_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:tint="?attr/actionBarTintColor"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M22,3H5A2,2 0 0,0 3,5V9H5V5H22V19H5V15H3V19A2,2 0 0,0 5,21H22A2,2 0 0,0 24,19V5A2,2 0 0,0 22,3M7,15V13H0V11H7V9L11,12L7,15M20,13H13V11H20V13M20,9H13V7H20V9M17,17H13V15H17V17Z" />
</vector>

View File

@ -0,0 +1,9 @@
<!-- drawable/page_previous_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:tint="?attr/actionBarTintColor"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M2,3H19A2,2 0 0,1 21,5V9H19V5H2V19H19V15H21V19A2,2 0 0,1 19,21H2A2,2 0 0,1 0,19V5A2,2 0 0,1 2,3M17,15V13H24V11H17V9L13,12L17,15M4,13H11V11H4V13M4,9H11V7H4V9M4,17H8V15H4V17Z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:end="96dp" android:start="64dp" android:drawable="@drawable/square_ripple"/> <item android:id="@+id/layer_full_width" android:end="0dp" android:start="64dp" android:drawable="@drawable/rect_ripple"/>
<item android:id="@+id/layer_one_item" android:end="48dp" android:start="64dp" android:drawable="@drawable/rect_ripple" />
</layer-list> </layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/fullRippleColor">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@color/fullRippleColor" />
</shape>
</item>
</ripple>

View File

@ -59,10 +59,26 @@
app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/chapters_button" 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:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_open_in_webview_24dp" /> app:srcCompat="@drawable/ic_open_in_webview_24dp" />
<ImageButton
android:id="@+id/double_page"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/next_title"
android:padding="@dimen/material_layout_keylines_screen_edge_margin"
app:tint="?actionBarTintColor"
app:layout_constraintHorizontal_chainStyle="spread"
android:tooltipText="@string/double_pages"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/webview_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/display_options"
app:srcCompat="@drawable/ic_book_open_variant_24dp" />
<ImageButton <ImageButton
android:id="@+id/display_options" android:id="@+id/display_options"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -74,7 +90,7 @@
app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_chainStyle="spread"
android:tooltipText="@string/display_options" android:tooltipText="@string/display_options"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/webview_button" app:layout_constraintStart_toEndOf="@id/double_page"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_tune_24dp" /> app:srcCompat="@drawable/ic_tune_24dp" />

View File

@ -46,12 +46,12 @@
<TextView <TextView
android:id="@+id/left_page_text" android:id="@+id/left_page_text"
android:layout_width="32dp" android:layout_width="48dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:textColor="@color/textColorPrimary" android:textColor="@color/textColorPrimary"
android:textSize="15sp" android:textSize="15sp"
tools:text="1" /> tools:text="12-14" />
<eu.kanade.tachiyomi.ui.reader.ReaderSeekBar <eu.kanade.tachiyomi.ui.reader.ReaderSeekBar
android:id="@+id/page_seekbar" android:id="@+id/page_seekbar"
@ -63,7 +63,7 @@
<TextView <TextView
android:id="@+id/right_page_text" android:id="@+id/right_page_text"
android:layout_width="32dp" android:layout_width="48dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:textColor="@color/textColorPrimary" android:textColor="@color/textColorPrimary"

View File

@ -40,6 +40,14 @@
app:title="@string/scale_type" app:title="@string/scale_type"
android:entries="@array/image_scale_type" /> android:entries="@array/image_scale_type" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/page_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:title="@string/page_layout"
android:entries="@array/page_layouts" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/extend_past_cutout" android:id="@+id/extend_past_cutout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -56,7 +64,6 @@
app:title="@string/zoom_start_position" app:title="@string/zoom_start_position"
android:entries="@array/zoom_start" /> android:entries="@array/zoom_start" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/crop_borders" android:id="@+id/crop_borders"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -2,4 +2,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_shift_double_page"
android:icon="@drawable/ic_page_next_outline_24dp"
android:title="@string/shift_one_page_over"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@ -70,6 +70,12 @@
<item>@string/right_and_left_nav</item> <item>@string/right_and_left_nav</item>
</string-array> </string-array>
<string-array name="page_layouts">
<item>@string/single_page</item>
<item>@string/double_pages</item>
<item>@string/automatic</item>
</string-array>
<string-array name="invert_tapping_mode"> <string-array name="invert_tapping_mode">
<item>@string/none</item> <item>@string/none</item>
<item>@string/horizontally</item> <item>@string/horizontally</item>

View File

@ -165,6 +165,7 @@
<string name="hide_category_hopper">Hide category hopper</string> <string name="hide_category_hopper">Hide category hopper</string>
<string name="hide_hopper_on_scroll">Hide category hopper on scroll</string> <string name="hide_hopper_on_scroll">Hide category hopper on scroll</string>
<string name="more_library_settings">More library settings</string> <string name="more_library_settings">More library settings</string>
<string name="shift_one_page_over">Shift one page over</string>
<!-- Library update service notifications --> <!-- Library update service notifications -->
<string name="new_chapters_found">New chapters found</string> <string name="new_chapters_found">New chapters found</string>
@ -279,9 +280,6 @@
<!-- Reader --> <!-- Reader -->
<string name="custom_filter">Custom filter</string> <string name="custom_filter">Custom filter</string>
<string name="set_as_cover">Set as cover</string> <string name="set_as_cover">Set as cover</string>
<string name="set_page_as_cover">Set page as cover</string>
<string name="share_page">Share page</string>
<string name="save_page">Save page</string>
<string name="_details">%1$s details</string> <string name="_details">%1$s details</string>
<string name="reader_settings">Reader settings</string> <string name="reader_settings">Reader settings</string>
<string name="set_as_default_for_all">Set as default for all</string> <string name="set_as_default_for_all">Set as default for all</string>
@ -306,6 +304,14 @@
<string name="next_chapter">Next chapter</string> <string name="next_chapter">Next chapter</string>
<string name="previous_chapter">Previous chapter</string> <string name="previous_chapter">Previous chapter</string>
<string name="set_first_page_as_cover">Set first page as cover</string>
<string name="share_first_page">Share first page</string>
<string name="save_first_page">Save first page</string>
<string name="set_second_page_as_cover">Set second page as cover</string>
<string name="share_second_page">Share second page</string>
<string name="save_second_page">Save second page</string>
<!-- Reader settings --> <!-- Reader settings -->
<string name="fullscreen">Fullscreen</string> <string name="fullscreen">Fullscreen</string>
<string name="animate_page_transitions">Animate page transitions</string> <string name="animate_page_transitions">Animate page transitions</string>
@ -352,6 +358,10 @@
<string name="original_size">Original size</string> <string name="original_size">Original size</string>
<string name="smart_fit">Smart fit</string> <string name="smart_fit">Smart fit</string>
<string name="zoom_start_position">Zoom start position</string> <string name="zoom_start_position">Zoom start position</string>
<string name="double_pages">Double pages</string>
<string name="single_page">Single page</string>
<string name="switch_to_double">Switch to double pages</string>
<string name="switch_to_single">Switch to single page</string>
<string name="force_portrait">Force portrait</string> <string name="force_portrait">Force portrait</string>
<string name="force_landscape">Force landscape</string> <string name="force_landscape">Force landscape</string>
<string name="red_initial">R</string> <string name="red_initial">R</string>
@ -382,6 +392,9 @@
<string name="pad_cutout_areas">Pad cutout areas</string> <string name="pad_cutout_areas">Pad cutout areas</string>
<string name="start_past_cutout">Start past cutout</string> <string name="start_past_cutout">Start past cutout</string>
<string name="ignore_cutout_areas">Ignore cutout areas</string> <string name="ignore_cutout_areas">Ignore cutout areas</string>
<string name="page_layout">Page layout</string>
<string name="automatic_can_still_switch">While using automatic page layout, you can still switch between layouts while reading without overriding this setting</string>
<string name="automatic_orientation">Automatic (based on orientation)</string>
<!-- Manga details --> <!-- Manga details -->
<string name="about_this_">About this %1$s</string> <string name="about_this_">About this %1$s</string>
@ -783,6 +796,7 @@
<string name="auto">Auto</string> <string name="auto">Auto</string>
<string name="automatic">Automatic</string> <string name="automatic">Automatic</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="beta">BETA</string>
<string name="bottom">Bottom</string> <string name="bottom">Bottom</string>
<string name="bug_report">Report a Bug</string> <string name="bug_report">Report a Bug</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>