Double page spread

(cherry picked from commit 7832d1abe1fdcdb962f388e5a86dd3fcecad6712)
This commit is contained in:
Jobobby04 2021-05-22 18:41:04 -04:00
parent 3192140421
commit 3053bf9d5d
19 changed files with 794 additions and 73 deletions

View File

@ -374,4 +374,8 @@ object PreferenceKeys {
const val hideUpdatesButton = "pref_hide_updates_button"
const val hideHistoryButton = "pref_hide_history_button"
const val pageLayout = "page_layout"
const val invertDoublePages = "invert_double_pages"
}

View File

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
@ -489,4 +490,8 @@ class PreferencesHelper(val context: Context) {
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, false)
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PagerConfig.PageLayout.AUTOMATIC)
fun invertDoublePages() = flowPrefs.getBoolean(Keys.invertDoublePages, false)
}

View File

@ -58,6 +58,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
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
@ -66,6 +67,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar
@ -114,6 +116,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
const val SHIFT_DOUBLE_PAGES = "shiftingDoublePages"
const val SHIFTED_PAGE_INDEX = "shiftedPageIndex"
const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex"
}
private val preferences: PreferencesHelper by injectLazy()
@ -143,6 +149,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
private val autoScrollFlow = MutableSharedFlow<Unit>()
private var autoScrollJob: Job? = null
private val sourceManager: SourceManager by injectLazy()
private var lastShiftDoubleState: Boolean? = null
private var indexPageToShift: Int? = null
private var indexChapterToShift: Long? = null
// SY <--
/**
@ -192,6 +202,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// --> EH
ehUtilsVisible = savedInstanceState.getBoolean(::ehUtilsVisible.name)
// <-- EH
// SY -->
lastShiftDoubleState = savedInstanceState.get(SHIFT_DOUBLE_PAGES) as? Boolean
indexPageToShift = savedInstanceState.get(SHIFTED_PAGE_INDEX) as? Int
indexChapterToShift = savedInstanceState.get(SHIFTED_CHAP_INDEX) as? Long
// SY <--
}
config = ReaderConfig()
@ -262,6 +277,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// EXH -->
outState.putBoolean(::ehUtilsVisible.name, ehUtilsVisible)
// EXH <--
// SY -->
(viewer as? PagerViewer)?.let { pViewer ->
val config = pViewer.config
outState.putBoolean(SHIFT_DOUBLE_PAGES, config.shiftDoublePage)
if (config.shiftDoublePage && config.doublePages) {
pViewer.getShiftedPage()?.let {
outState.putInt(SHIFTED_PAGE_INDEX, it.index)
outState.putLong(SHIFTED_CHAP_INDEX, it.chapter.chapter.id ?: 0L)
}
}
}
// SY <--
if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange()
}
@ -543,6 +570,30 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
}
}
with(binding.doublePage) {
setTooltip(R.string.page_layout)
setOnClickListener {
if (preferences.pageLayout().get() == PagerConfig.PageLayout.AUTOMATIC) {
(viewer as? PagerViewer)?.config?.let { config ->
config.doublePages = !config.doublePages
reloadChapters(config.doublePages, true)
}
updateBottomButtons()
} else {
preferences.pageLayout().set(1 - preferences.pageLayout().get())
}
}
}
with(binding.shiftPageButton) {
setTooltip(R.string.shift_double_pages)
setOnClickListener {
shiftDoublePages()
}
}
binding.expandEhButton.clicks()
.onEach {
ehUtilsVisible = !ehUtilsVisible
@ -723,7 +774,8 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
actionReadingMode.isVisible = ReaderBottomButton.ReadingMode.isIn(enabledButtons)
actionRotation.isVisible =
ReaderBottomButton.Rotation.isIn(enabledButtons)
// doublePage.isVisible = viewer is PagerViewer && ReaderBottomButton.PageLayout.isIn(enabledButtons)
doublePage.isVisible =
viewer is PagerViewer && ReaderBottomButton.PageLayout.isIn(enabledButtons) && !preferences.dualPageSplitPaged().get()
actionCropBorders.isVisible =
if (viewer is PagerViewer) {
ReaderBottomButton.CropBordersPager.isIn(enabledButtons)
@ -739,8 +791,44 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
ReaderBottomButton.WebView.isIn(enabledButtons)
actionChapterList.isVisible =
ReaderBottomButton.ViewChapters.isIn(enabledButtons)
// shiftPageButton.isVisible = ((viewer as? PagerViewer)?.config?.doublePages ?: false) && ReaderBottomButton.ShiftDoublePage.isIn(enabledButtons)
// binding.toolbar.menu.findItem(R.id.action_shift_double_page)?.isVisible = ((viewer as? PagerViewer)?.config?.doublePages ?: false) && !ReaderBottomButton.ShiftDoublePage.isIn(enabledButtons)
shiftPageButton.isVisible = (viewer as? PagerViewer)?.config?.doublePages ?: false
}
}
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.pageSeekbar.progress +
(currentChapter?.pages?.take(binding.pageSeekbar.progress)?.count { it.fullPage || it.isolatedPage } ?: 0)
) % 2 != 0
}
presenter.viewerChaptersRelay.value?.let {
pViewer.setChaptersDoubleShift(it)
}
}
private fun setDoublePageMode(viewer: PagerViewer) {
val currentOrientation = resources.configuration.orientation
viewer.config.doublePages = currentOrientation == Configuration.ORIENTATION_LANDSCAPE
}
private fun shiftDoublePages() {
(viewer as? PagerViewer)?.config?.let { config ->
config.shiftDoublePage = !config.shiftDoublePage
presenter.viewerChaptersRelay.value?.let {
(viewer as? PagerViewer)?.updateShifting()
(viewer as? PagerViewer)?.setChaptersDoubleShift(it)
invalidateOptionsMenu()
}
}
}
// EXH <--
@ -905,6 +993,13 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView())
// SY -->
if (newViewer is PagerViewer) {
if (preferences.pageLayout().get() == PagerConfig.PageLayout.AUTOMATIC) {
setDoublePageMode(newViewer)
}
lastShiftDoubleState?.let { newViewer.config.shiftDoublePage = it }
}
val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name))
if (preferences.useAutoWebtoon().get() && manga.readingModeType == ReadingModeType.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) {
readingModeToast?.cancel()
@ -980,6 +1075,25 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
*/
fun setChapters(viewerChapters: ViewerChapters) {
binding.pleaseWait.isVisible = false
// SY -->
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
} else if (lastShiftDoubleState != null) {
val currentChapter = viewerChapters.currChapter
(viewer as? PagerViewer)?.config?.shiftDoublePage = (
currentChapter.requestedPage +
(
currentChapter.pages?.take(currentChapter.requestedPage)
?.count { it.fullPage || it.isolatedPage } ?: 0
)
) % 2 != 0
}
// SY <--
viewer?.setChapters(viewerChapters)
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
@ -1045,25 +1159,31 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* bottom menu and delegates the change to the presenter.
*/
@SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage) {
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean = false) {
val newChapter = presenter.onPageSelected(page)
val pages = page.chapter.pages ?: return
val currentPage = if (hasExtraPage) {
if (resources.isLTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}"
} else {
"${page.number}"
}
// Set bottom page number
binding.pageNumber.text = "${page.number}/${pages.size}"
binding.pageNumber.text = "$currentPage/${pages.size}"
// binding.pageText.text = "${page.number}/${pages.size}"
// Set seekbar page number
if (viewer !is R2LPagerViewer) {
binding.leftPageText.text = "${page.number}"
binding.leftPageText.text = currentPage
binding.rightPageText.text = "${pages.size}"
} else {
binding.rightPageText.text = "${page.number}"
binding.rightPageText.text = currentPage
binding.leftPageText.text = "${pages.size}"
}
// SY -->
binding.abovePageText.text = "${page.number}"
binding.abovePageText.text = currentPage
binding.belowPageText.text = "${pages.size}"
// SY <--
@ -1080,11 +1200,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* 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) {
fun onPageLongTap(page: ReaderPage, extraPage: ReaderPage? = null) {
// EXH -->
try {
// EXH <--
ReaderPageSheet(this, page).show()
ReaderPageSheet(this, page, extraPage).show()
// EXH -->
} catch (e: WindowManager.BadTokenException) {
xLogE("Caught and ignoring reader page sheet launch exception!", e)
@ -1256,6 +1376,27 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
preferences.grayscale().asFlow()
.onEach { setGrayscale(it) }
.launchIn(lifecycleScope)
preferences.pageLayout().asFlow()
.drop(1)
.onEach { updateBottomButtons() }
.launchIn(lifecycleScope)
preferences.dualPageSplitPaged().asFlow()
.drop(1)
.onEach {
if (viewer !is PagerViewer) return@onEach
updateBottomButtons()
reloadChapters(
!it && when (preferences.pageLayout().get()) {
PagerConfig.PageLayout.DOUBLE_PAGES -> true
PagerConfig.PageLayout.AUTOMATIC -> resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
else -> false
},
true
)
}
.launchIn(lifecycleScope)
}
/**

View File

@ -14,7 +14,8 @@ import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
*/
class ReaderPageSheet(
private val activity: ReaderActivity,
private val page: ReaderPage
private val page: ReaderPage,
private val extraPage: ReaderPage? = null
) : BaseBottomSheetDialog(activity) {
private lateinit var binding: ReaderPageSheetBinding

View File

@ -11,10 +11,21 @@ open class ReaderPage(
// SY -->
var bg: Drawable? = 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,
// SY <--
var stream: (() -> InputStream)? = null
) : Page(index, url, imageUrl, null) {
open 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

@ -11,8 +11,7 @@ enum class ReaderBottomButton(val value: String, @StringRes val stringRes: Int)
CropBordersPager("cbp", R.string.pref_crop_borders_pager),
CropBordersContinuesVertical("cbc", R.string.pref_crop_borders_continuous_vertical),
CropBordersWebtoon("cbw", R.string.pref_crop_borders_webtoon),
// PageLayout("pl", R.string.page_layout),
// ShiftDoublePage("sdp", R.string.shift_double_pages)
PageLayout("pl", R.string.page_layout),
;
fun isIn(buttons: Collection<String>) = value in buttons
@ -22,7 +21,8 @@ enum class ReaderBottomButton(val value: String, @StringRes val stringRes: Int)
ViewChapters,
WebView,
CropBordersPager,
CropBordersContinuesVertical
CropBordersContinuesVertical,
PageLayout
).map { it.value }.toSet()
}
}

View File

@ -87,6 +87,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
// SY -->
binding.pagerPrefsGroup.pageTransitionsPager.bindToPreference(preferences.pageTransitionsPager())
binding.pagerPrefsGroup.pageLayout.bindToPreference(preferences.pageLayout())
// SY <--
}

View File

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.graphics.Color
import androidx.annotation.ColorInt
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@ -28,6 +30,8 @@ class PagerConfig(
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
var reloadChapterListener: ((Boolean) -> Unit)? = null
var imageScaleType = 1
private set
@ -39,6 +43,23 @@ class PagerConfig(
// SY -->
var usePageTransitions = false
var shiftDoublePage = false
var doublePages = preferences.pageLayout().get() == PageLayout.DOUBLE_PAGES && !preferences.dualPageSplitPaged().get()
set(value) {
field = value
if (!value) {
shiftDoublePage = false
}
}
var invertDoublePages = false
var autoDoublePages = preferences.pageLayout().get() == PageLayout.AUTOMATIC
@ColorInt
var pageCanvasColor = Color.WHITE
// SY <--
init {
@ -79,6 +100,33 @@ class PagerConfig(
// SY -->
preferences.pageTransitionsPager()
.register({ usePageTransitions = it }, { imagePropertyChangedListener?.invoke() })
preferences.readerTheme()
.register(
{
themeToColor(it)
},
{
themeToColor(it)
reloadChapterListener?.invoke(doublePages)
}
)
preferences.pageLayout()
.register(
{
autoDoublePages = it == PageLayout.AUTOMATIC
if (!autoDoublePages) {
doublePages = it == PageLayout.DOUBLE_PAGES && dualPageSplit == false
}
},
{
autoDoublePages = it == PageLayout.AUTOMATIC
if (!autoDoublePages) {
doublePages = it == PageLayout.DOUBLE_PAGES && dualPageSplit == false
}
reloadChapterListener?.invoke(doublePages)
}
)
// SY <--
}
@ -126,4 +174,18 @@ class PagerConfig(
enum class ZoomType {
Left, Center, Right
}
object PageLayout {
const val SINGLE_PAGE = 0
const val DOUBLE_PAGES = 1
const val AUTOMATIC = 2
}
fun themeToColor(theme: Int) {
pageCanvasColor = when (theme) {
1 -> Color.BLACK
2 -> 0x202125
else -> Color.WHITE
}
}
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.graphics.BitmapFactory
import android.graphics.PointF
import android.graphics.drawable.Animatable
import android.view.GestureDetector
@ -27,16 +28,22 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber
import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
/**
* View of the ViewPager that contains a page of a chapter.
@ -44,14 +51,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.
@ -88,14 +96,34 @@ 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
// SY -->
var status: Int = 0
var extraStatus: Int = 0
var progress: Int = 0
var extraProgress: Int = 0
private var skipExtra = false
var scope: CoroutineScope? = null
// SY <--
init {
addView(progressBar)
scope = CoroutineScope(Job() + Dispatchers.Default)
observeStatus()
}
@ -105,8 +133,10 @@ class PagerPageHolder(
@SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
unsubscribeProgress()
unsubscribeStatus()
unsubscribeProgress(1)
unsubscribeProgress(2)
unsubscribeStatus(1)
unsubscribeStatus(2)
unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null)
}
@ -122,7 +152,19 @@ 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)
}
}
/**
@ -139,6 +181,20 @@ class PagerPageHolder(
.subscribe { value -> progressBar.setProgress(value) }
}
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 * 0.95f).roundToInt())
}
}
/**
* Called when the status of the page changes.
*
@ -153,12 +209,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)
}
}
}
@ -166,17 +250,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
}
/**
@ -219,18 +305,31 @@ class PagerPageHolder(
*/
private fun setImage() {
progressBar.isVisible = true
progressBar.completeAndFadeOut()
progressBar.isVisible = true
if (extraPage == null) {
progressBar.completeAndFadeOut()
} else {
progressBar.setProgress(95)
}
retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false
unsubscribeReadImageHeader()
val streamFn = page.stream ?: return
val streamFn2 = extraPage?.stream
var openStream: InputStream? = null
readImageHeaderSubscription = Observable
.fromCallable {
val stream = streamFn().buffered(16)
openStream = process(item, stream)
// SY -->
val stream2 = if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null
openStream = if (viewer.config.dualPageSplit) {
process(item.first, stream)
} else {
mergePages(stream, stream2)
}
// SY <--
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
}
@ -273,6 +372,81 @@ class PagerPageHolder(
return splitInHalf(imageStream)
}
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 = try {
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
} catch (e: Exception) {
imageStream2.close()
imageStream.close()
page.fullPage = true
skipExtra = true
Timber.e("Cannot combine pages ${e.message}")
return imageBytes.inputStream()
}
scope?.launchUI { progressBar.setProgress(96) }
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 = try {
BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
} catch (e: Exception) {
imageStream2.close()
imageStream.close()
extraPage?.fullPage = true
skipExtra = true
page.isolatedPage = true
Timber.e("Cannot combine pages ${e.message}")
return imageBytes.inputStream()
}
scope?.launchUI { progressBar.setProgress(97) }
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 isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages)
imageStream.close()
imageStream2.close()
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, viewer.config.pageCanvasColor) {
scope?.launchUI {
if (it == 100) {
progressBar.completeAndFadeOut()
} else {
progressBar.setProgress(it)
}
}
}
}
private fun splitInHalf(imageStream: InputStream): InputStream {
var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT

View File

@ -72,6 +72,16 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
}
}
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.isVisible = false // Don't layout the pager yet
pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
@ -79,15 +89,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
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
}
}
// SY -->
pagerListener
// SY <--
)
pager.tapListener = f@{ event ->
if (!config.tappingEnabled) {
@ -107,9 +111,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
}
}
@ -122,6 +128,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
}
}
config.reloadChapterListener = {
activity.reloadChapters(it)
}
config.imagePropertyChangedListener = {
refreshAdapter()
}
@ -153,13 +163,13 @@ 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)
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)
}
}
}
@ -187,10 +197,10 @@ 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) {
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, hasExtraPage: Boolean) {
val pages = page.chapter.pages ?: return
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
activity.onPageSelected(page)
activity.onPageSelected(page, hasExtraPage)
// Skip preload on inserts it causes unwanted page jumping
if (page is InsertPage) {
@ -240,7 +250,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
*/
private fun setChaptersInternal(chapters: ViewerChapters) {
Timber.d("setChaptersInternal")
val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull(pager.currentItem) is ChapterTransition
val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition
adapter.setChapters(chapters, forceTransition)
// Layout the pager once a chapter is being set
@ -257,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")
@ -399,4 +417,22 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
private fun cleanupPageSplit() {
adapter.cleanupPageSplit()
}
// SY -->
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.getOrNull(pager.currentItem)?.first as? ReaderPage
}
fun getShiftedPage(): ReaderPage? = adapter.pageToShift
// SY <--
}

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
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.
@ -17,11 +18,16 @@ import timber.log.Timber
class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/**
* List of currently set items.
* Paired list of currently set items.
*/
var items: MutableList<Any> = mutableListOf()
var joinedItems: MutableList<Pair<Any, Any?>> = mutableListOf()
private set
/**
* Single list of items
*/
private var subItems: MutableList<Any> = mutableListOf()
/**
* Holds preprocessed items so they don't get removed when changing chapter
*/
@ -32,6 +38,15 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
var currentChapter: ReaderChapter? = null
// SY -->
/** 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
// SY <--
/**
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
* next/previous chapter to allow seamless transitions and inverting the pages if the viewer
@ -102,15 +117,20 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
}
// Resets double-page splits, else insert pages get misplaced
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
if (viewer is R2LPagerViewer) {
newItems.reverse()
}
subItems.filterIsInstance<InsertPage>().also { subItems.removeAll(it) }
preprocessed = mutableMapOf()
items = newItems
notifyDataSetChanged()
subItems = newItems.toMutableList()
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)
// Will skip insert page otherwise
if (insertPageLastPage != null) {
@ -122,15 +142,17 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
* 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")
}
@ -141,7 +163,7 @@ 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.indexOf(view.item)
if (position != -1) {
return position
} else {
@ -154,7 +176,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
fun onPageSplit(currentPage: Any?, newPage: InsertPage) {
if (currentPage !is ReaderPage) return
val currentIndex = items.indexOf(currentPage)
val currentIndex = joinedItems.indexOfFirst { it.first == currentPage }
// Put aside preprocessed pages for next chapter so they don't get removed when changing chapter
if (currentPage.chapter.chapter.id != currentChapter?.chapter?.id) {
@ -169,23 +191,157 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
}
// It will enter a endless cycle of insert pages
if (viewer is R2LPagerViewer && placeAtIndex - 1 >= 0 && items[placeAtIndex - 1] is InsertPage) {
if (viewer is R2LPagerViewer && placeAtIndex - 1 >= 0 && joinedItems[placeAtIndex - 1].first is InsertPage) {
return
}
// Same here it will enter a endless cycle of insert pages
if (items[placeAtIndex] is InsertPage) {
if (joinedItems[placeAtIndex].first is InsertPage) {
return
}
items.add(placeAtIndex, newPage)
joinedItems.add(placeAtIndex, newPage to null)
notifyDataSetChanged()
}
fun cleanupPageSplit() {
val insertPages = items.filterIsInstance(InsertPage::class.java)
items.removeAll(insertPages)
val insertPages = joinedItems.filter { it.first is InsertPage }
joinedItems.removeAll(insertPages)
notifyDataSetChanged()
}
// SY -->
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.take(index).indexOfLast { 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
}
var index = joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
if (newPage is ChapterTransition && index == -1) {
val newerPage = if (newPage is ChapterTransition.Next) {
joinedItems.filter {
(it.first as? ReaderPage)?.chapter == newPage.to
}.minByOrNull { (it.first as? ReaderPage)?.index ?: Int.MAX_VALUE }?.first
} else {
joinedItems.filter {
(it.first as? ReaderPage)?.chapter == newPage.to
}.maxByOrNull { (it.first as? ReaderPage)?.index ?: Int.MIN_VALUE }?.first
}
index = joinedItems.indexOfFirst { it.first == newerPage || it.second == newerPage }
}
viewer.pager.setCurrentItem(index, false)
}
// SY <--
}

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference
@ -515,6 +516,24 @@ class SettingsReaderController : SettingsController() {
ReaderBottomButtonsDialog().showDialog(router)
}
}
intListPreference {
key = Keys.pageLayout
titleRes = R.string.page_layout
summaryRes = R.string.automatic_can_still_switch
entriesRes = arrayOf(
R.string.single_page,
R.string.double_pages,
R.string.automatic_orientation
)
entryValues = arrayOf("0", "1", "2")
defaultValue = 2
}
switchPreference {
key = Keys.invertDoublePages
titleRes = R.string.invert_double_pages
defaultValue = false
preferences.pageLayout().asImmediateFlow { isVisible = it != PagerConfig.PageLayout.SINGLE_PAGE }
}
}
// EXH <--
}

View File

@ -10,6 +10,7 @@ import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.createBitmap
@ -20,6 +21,7 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URLConnection
import kotlin.math.abs
import kotlin.math.max
object ImageUtil {
@ -381,4 +383,45 @@ object ImageUtil {
private fun Int.isWhite(): Boolean =
red + blue + green > 740
fun mergeBitmaps(
imageBitmap: Bitmap,
imageBitmap2: Bitmap,
isLTR: Boolean,
@ColorInt background: Int = Color.WHITE,
progressCallback: ((Int) -> Unit)? = null
): ByteArrayInputStream {
val height = imageBitmap.height
val width = imageBitmap.width
val height2 = imageBitmap2.height
val width2 = imageBitmap2.width
val maxHeight = max(height, height2)
val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
canvas.drawColor(background)
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)
progressCallback?.invoke(98)
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)
progressCallback?.invoke(99)
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray())
}
private val Bitmap.rect: Rect
get() = Rect(0, 0, width, height)
}

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="@android:color/black">
<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,9 @@
<!-- drawable/page_next_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:tint="@android:color/black"
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

@ -440,13 +440,41 @@
android:background="@drawable/ripple_regular"
android:contentDescription="@string/pref_rotation_type"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_settings"
app:layout_constraintEnd_toStartOf="@id/double_page"
app:layout_constraintStart_toEndOf="@+id/action_crop_borders"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_screen_rotation_24dp"
app:tint="?attr/colorOnPrimary" />
<ImageButton
android:id="@+id/double_page"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/ripple_regular"
android:contentDescription="@string/page_layout"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/shift_page_button"
app:layout_constraintStart_toEndOf="@+id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_book_open_variant_24dp"
app:tint="?attr/colorOnPrimary" />
<ImageButton
android:id="@+id/shift_page_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/ripple_regular"
android:contentDescription="@string/shift_double_pages"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_settings"
app:layout_constraintStart_toEndOf="@+id/double_page"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_page_next_outline_24dp"
app:tint="?attr/colorOnPrimary" />
<ImageButton
android:id="@+id/action_settings"
android:layout_width="wrap_content"
@ -455,7 +483,7 @@
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintStart_toEndOf="@id/shift_page_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_settings_24dp"

View File

@ -43,6 +43,13 @@
android:entries="@array/zoom_start"
app:title="@string/pref_zoom_start" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/page_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/page_layouts"
app:title="@string/page_layout" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/crop_borders"
android:layout_width="match_parent"

View File

@ -15,4 +15,10 @@
<item>@string/dropped</item>
<item>@string/repeating</item>
</string-array>
<string-array name="page_layouts">
<item>@string/single_page</item>
<item>@string/double_pages</item>
<item>@string/automatic_orientation</item>
</string-array>
</resources>

View File

@ -279,6 +279,15 @@
<!-- Auto Webtoon Mode -->
<string name="eh_auto_webtoon_snack">Reading webtoon style</string>
<!-- Double page spread -->
<string name="page_layout">Page layout</string>
<string name="shift_double_pages">Shift one page over</string>
<string name="double_pages">Double pages</string>
<string name="single_page">Single page</string>
<string name="automatic_orientation">Automatic (based on orientation)</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="invert_double_pages">Invert double pages</string>
<!-- Manga Page -->
<!-- Manga Info -->
<string name="az_recommends">See Recommendations</string>