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 hideUpdatesButton = "pref_hide_updates_button"
const val hideHistoryButton = "pref_hide_history_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.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -489,4 +490,8 @@ class PreferencesHelper(val context: Context) {
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false) fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, 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.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer 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.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
@ -66,6 +67,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.hasDisplayCutout 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.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar import eu.kanade.tachiyomi.util.view.hideBar
@ -114,6 +116,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 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() private val preferences: PreferencesHelper by injectLazy()
@ -143,6 +149,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
private val autoScrollFlow = MutableSharedFlow<Unit>() private val autoScrollFlow = MutableSharedFlow<Unit>()
private var autoScrollJob: Job? = null private var autoScrollJob: Job? = null
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private var lastShiftDoubleState: Boolean? = null
private var indexPageToShift: Int? = null
private var indexChapterToShift: Long? = null
// SY <-- // SY <--
/** /**
@ -192,6 +202,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// --> EH // --> EH
ehUtilsVisible = savedInstanceState.getBoolean(::ehUtilsVisible.name) ehUtilsVisible = savedInstanceState.getBoolean(::ehUtilsVisible.name)
// <-- EH // <-- 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() config = ReaderConfig()
@ -262,6 +277,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// EXH --> // EXH -->
outState.putBoolean(::ehUtilsVisible.name, ehUtilsVisible) outState.putBoolean(::ehUtilsVisible.name, ehUtilsVisible)
// EXH <-- // 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) { if (!isChangingConfigurations) {
presenter.onSaveInstanceStateNonConfigurationChange() 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() binding.expandEhButton.clicks()
.onEach { .onEach {
ehUtilsVisible = !ehUtilsVisible ehUtilsVisible = !ehUtilsVisible
@ -723,7 +774,8 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
actionReadingMode.isVisible = ReaderBottomButton.ReadingMode.isIn(enabledButtons) actionReadingMode.isVisible = ReaderBottomButton.ReadingMode.isIn(enabledButtons)
actionRotation.isVisible = actionRotation.isVisible =
ReaderBottomButton.Rotation.isIn(enabledButtons) 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 = actionCropBorders.isVisible =
if (viewer is PagerViewer) { if (viewer is PagerViewer) {
ReaderBottomButton.CropBordersPager.isIn(enabledButtons) ReaderBottomButton.CropBordersPager.isIn(enabledButtons)
@ -739,8 +791,44 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
ReaderBottomButton.WebView.isIn(enabledButtons) ReaderBottomButton.WebView.isIn(enabledButtons)
actionChapterList.isVisible = actionChapterList.isVisible =
ReaderBottomButton.ViewChapters.isIn(enabledButtons) ReaderBottomButton.ViewChapters.isIn(enabledButtons)
// shiftPageButton.isVisible = ((viewer as? PagerViewer)?.config?.doublePages ?: false) && ReaderBottomButton.ShiftDoublePage.isIn(enabledButtons) shiftPageButton.isVisible = (viewer as? PagerViewer)?.config?.doublePages ?: false
// binding.toolbar.menu.findItem(R.id.action_shift_double_page)?.isVisible = ((viewer as? PagerViewer)?.config?.doublePages ?: false) && !ReaderBottomButton.ShiftDoublePage.isIn(enabledButtons) }
}
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 <-- // EXH <--
@ -905,6 +993,13 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView()) binding.viewerContainer.addView(newViewer.getView())
// SY --> // 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)) 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) { if (preferences.useAutoWebtoon().get() && manga.readingModeType == ReadingModeType.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) {
readingModeToast?.cancel() readingModeToast?.cancel()
@ -980,6 +1075,25 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
*/ */
fun setChapters(viewerChapters: ViewerChapters) { fun setChapters(viewerChapters: ViewerChapters) {
binding.pleaseWait.isVisible = false 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) viewer?.setChapters(viewerChapters)
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name 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. * bottom menu and delegates the change to the presenter.
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun onPageSelected(page: ReaderPage) { fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean = false) {
val newChapter = presenter.onPageSelected(page) val newChapter = presenter.onPageSelected(page)
val pages = page.chapter.pages ?: return 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 // Set bottom page number
binding.pageNumber.text = "${page.number}/${pages.size}" binding.pageNumber.text = "$currentPage/${pages.size}"
// binding.pageText.text = "${page.number}/${pages.size}" // binding.pageText.text = "${page.number}/${pages.size}"
// Set seekbar page number // Set seekbar page number
if (viewer !is R2LPagerViewer) { if (viewer !is R2LPagerViewer) {
binding.leftPageText.text = "${page.number}" binding.leftPageText.text = currentPage
binding.rightPageText.text = "${pages.size}" binding.rightPageText.text = "${pages.size}"
} else { } else {
binding.rightPageText.text = "${page.number}" binding.rightPageText.text = currentPage
binding.leftPageText.text = "${pages.size}" binding.leftPageText.text = "${pages.size}"
} }
// SY --> // SY -->
binding.abovePageText.text = "${page.number}" binding.abovePageText.text = currentPage
binding.belowPageText.text = "${pages.size}" binding.belowPageText.text = "${pages.size}"
// SY <-- // 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 * 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) {
// EXH --> // EXH -->
try { try {
// EXH <-- // EXH <--
ReaderPageSheet(this, page).show() ReaderPageSheet(this, page, extraPage).show()
// EXH --> // EXH -->
} catch (e: WindowManager.BadTokenException) { } catch (e: WindowManager.BadTokenException) {
xLogE("Caught and ignoring reader page sheet launch exception!", e) xLogE("Caught and ignoring reader page sheet launch exception!", e)
@ -1256,6 +1376,27 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
preferences.grayscale().asFlow() preferences.grayscale().asFlow()
.onEach { setGrayscale(it) } .onEach { setGrayscale(it) }
.launchIn(lifecycleScope) .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( class ReaderPageSheet(
private val activity: ReaderActivity, private val activity: ReaderActivity,
private val page: ReaderPage private val page: ReaderPage,
private val extraPage: ReaderPage? = null
) : BaseBottomSheetDialog(activity) { ) : BaseBottomSheetDialog(activity) {
private lateinit var binding: ReaderPageSheetBinding private lateinit var binding: ReaderPageSheetBinding

View File

@ -11,10 +11,21 @@ open class ReaderPage(
// SY --> // SY -->
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,
// SY <-- // SY <--
var stream: (() -> InputStream)? = null var stream: (() -> InputStream)? = null
) : Page(index, url, imageUrl, null) { ) : Page(index, url, imageUrl, null) {
open lateinit var chapter: ReaderChapter 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), CropBordersPager("cbp", R.string.pref_crop_borders_pager),
CropBordersContinuesVertical("cbc", R.string.pref_crop_borders_continuous_vertical), CropBordersContinuesVertical("cbc", R.string.pref_crop_borders_continuous_vertical),
CropBordersWebtoon("cbw", R.string.pref_crop_borders_webtoon), CropBordersWebtoon("cbw", R.string.pref_crop_borders_webtoon),
// PageLayout("pl", R.string.page_layout), PageLayout("pl", R.string.page_layout),
// ShiftDoublePage("sdp", R.string.shift_double_pages)
; ;
fun isIn(buttons: Collection<String>) = value in buttons fun isIn(buttons: Collection<String>) = value in buttons
@ -22,7 +21,8 @@ enum class ReaderBottomButton(val value: String, @StringRes val stringRes: Int)
ViewChapters, ViewChapters,
WebView, WebView,
CropBordersPager, CropBordersPager,
CropBordersContinuesVertical CropBordersContinuesVertical,
PageLayout
).map { it.value }.toSet() ).map { it.value }.toSet()
} }
} }

View File

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

View File

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager 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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@ -28,6 +30,8 @@ class PagerConfig(
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
var reloadChapterListener: ((Boolean) -> Unit)? = null
var imageScaleType = 1 var imageScaleType = 1
private set private set
@ -39,6 +43,23 @@ class PagerConfig(
// SY --> // SY -->
var usePageTransitions = false 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 <-- // SY <--
init { init {
@ -79,6 +100,33 @@ class PagerConfig(
// SY --> // SY -->
preferences.pageTransitionsPager() preferences.pageTransitionsPager()
.register({ usePageTransitions = it }, { imagePropertyChangedListener?.invoke() }) .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 <-- // SY <--
} }
@ -126,4 +174,18 @@ class PagerConfig(
enum class ZoomType { enum class ZoomType {
Left, Center, Right 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 package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.BitmapFactory
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.view.GestureDetector 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.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.ViewPagerAdapter import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
/** /**
* View of the ViewPager that contains a page of a chapter. * View of the ViewPager that contains a page of a chapter.
@ -44,14 +51,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.
@ -88,14 +96,34 @@ 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
// 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 { init {
addView(progressBar) addView(progressBar)
scope = CoroutineScope(Job() + Dispatchers.Default)
observeStatus() observeStatus()
} }
@ -105,8 +133,10 @@ class PagerPageHolder(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
unsubscribeProgress() unsubscribeProgress(1)
unsubscribeStatus() unsubscribeProgress(2)
unsubscribeStatus(1)
unsubscribeStatus(2)
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null) subsamplingImageView?.setOnImageEventListener(null)
} }
@ -122,7 +152,19 @@ 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)
}
} }
/** /**
@ -139,6 +181,20 @@ class PagerPageHolder(
.subscribe { value -> progressBar.setProgress(value) } .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. * Called when the status of the page changes.
* *
@ -153,12 +209,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)
} }
} }
} }
@ -166,17 +250,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
} }
/** /**
@ -219,18 +305,31 @@ class PagerPageHolder(
*/ */
private fun setImage() { private fun setImage() {
progressBar.isVisible = true progressBar.isVisible = true
progressBar.completeAndFadeOut() progressBar.isVisible = true
if (extraPage == null) {
progressBar.completeAndFadeOut()
} else {
progressBar.setProgress(95)
}
retryButton?.isVisible = false retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false decodeErrorLayout?.isVisible = false
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 = 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 ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
} }
@ -273,6 +372,81 @@ class PagerPageHolder(
return splitInHalf(imageStream) 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 { private fun splitInHalf(imageStream: InputStream): InputStream {
var side = when { var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT 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 { init {
pager.isVisible = false // Don't layout the pager yet pager.isVisible = false // Don't layout the pager yet
pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 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.id = R.id.reader_pager
pager.adapter = adapter pager.adapter = adapter
pager.addOnPageChangeListener( pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() { // SY -->
override fun onPageSelected(position: Int) { pagerListener
onPageChange(position) // SY <--
}
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) {
@ -107,9 +111,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
} }
} }
@ -122,6 +128,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
} }
config.reloadChapterListener = {
activity.reloadChapters(it)
}
config.imagePropertyChangedListener = { config.imagePropertyChangedListener = {
refreshAdapter() 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 * Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
*/ */
private fun onPageChange(position: Int) { private 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)
} }
} }
} }
@ -187,10 +197,10 @@ 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) {
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}")
activity.onPageSelected(page) activity.onPageSelected(page, hasExtraPage)
// Skip preload on inserts it causes unwanted page jumping // Skip preload on inserts it causes unwanted page jumping
if (page is InsertPage) { if (page is InsertPage) {
@ -240,7 +250,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(pager.currentItem) is ChapterTransition val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition
adapter.setChapters(chapters, forceTransition) adapter.setChapters(chapters, forceTransition)
// Layout the pager once a chapter is being set // 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) { 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")
@ -399,4 +417,22 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
private fun cleanupPageSplit() { private fun cleanupPageSplit() {
adapter.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.ui.reader.viewer.hasMissingChapters
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.
@ -17,11 +18,16 @@ 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: MutableList<Any> = mutableListOf() var joinedItems: MutableList<Pair<Any, Any?>> = mutableListOf()
private set private set
/**
* Single list of items
*/
private var subItems: MutableList<Any> = mutableListOf()
/** /**
* Holds preprocessed items so they don't get removed when changing chapter * 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 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 * 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 * 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 // Resets double-page splits, else insert pages get misplaced
items.filterIsInstance<InsertPage>().also { items.removeAll(it) } subItems.filterIsInstance<InsertPage>().also { subItems.removeAll(it) }
if (viewer is R2LPagerViewer) {
newItems.reverse()
}
preprocessed = mutableMapOf() preprocessed = mutableMapOf()
items = newItems subItems = newItems.toMutableList()
notifyDataSetChanged()
var useSecondPage = false
if (shifted != viewer.config.shiftDoublePage || (doubledUp != viewer.config.doublePages && doubledUp)) {
if (shifted && (doubledUp == viewer.config.doublePages)) {
useSecondPage = true
}
shifted = viewer.config.shiftDoublePage
}
doubledUp = viewer.config.doublePages
setJoinedItems(useSecondPage)
// Will skip insert page otherwise // Will skip insert page otherwise
if (insertPageLastPage != null) { if (insertPageLastPage != null) {
@ -122,15 +142,17 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
* 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")
} }
@ -141,7 +163,7 @@ 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.indexOf(view.item)
if (position != -1) { if (position != -1) {
return position return position
} else { } else {
@ -154,7 +176,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
fun onPageSplit(currentPage: Any?, newPage: InsertPage) { fun onPageSplit(currentPage: Any?, newPage: InsertPage) {
if (currentPage !is ReaderPage) return 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 // Put aside preprocessed pages for next chapter so they don't get removed when changing chapter
if (currentPage.chapter.chapter.id != currentChapter?.chapter?.id) { 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 // 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 return
} }
// Same here it will enter a endless cycle of insert pages // Same here it will enter a endless cycle of insert pages
if (items[placeAtIndex] is InsertPage) { if (joinedItems[placeAtIndex].first is InsertPage) {
return return
} }
items.add(placeAtIndex, newPage) joinedItems.add(placeAtIndex, newPage to null)
notifyDataSetChanged() notifyDataSetChanged()
} }
fun cleanupPageSplit() { fun cleanupPageSplit() {
val insertPages = items.filterIsInstance(InsertPage::class.java) val insertPages = joinedItems.filter { it.first is InsertPage }
items.removeAll(insertPages) joinedItems.removeAll(insertPages)
notifyDataSetChanged() 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.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType 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.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.intListPreference
@ -515,6 +516,24 @@ class SettingsReaderController : SettingsController() {
ReaderBottomButtonsDialog().showDialog(router) 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 <-- // EXH <--
} }

View File

@ -10,6 +10,7 @@ import android.graphics.Rect
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import androidx.core.graphics.blue import androidx.core.graphics.blue
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@ -20,6 +21,7 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.net.URLConnection import java.net.URLConnection
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
object ImageUtil { object ImageUtil {
@ -381,4 +383,45 @@ object ImageUtil {
private fun Int.isWhite(): Boolean = private fun Int.isWhite(): Boolean =
red + blue + green > 740 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:background="@drawable/ripple_regular"
android:contentDescription="@string/pref_rotation_type" android:contentDescription="@string/pref_rotation_type"
android:padding="@dimen/screen_edge_margin" 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_constraintStart_toEndOf="@+id/action_crop_borders"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_screen_rotation_24dp" app:srcCompat="@drawable/ic_screen_rotation_24dp"
app:tint="?attr/colorOnPrimary" /> 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 <ImageButton
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -455,7 +483,7 @@
android:contentDescription="@string/action_settings" android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin" android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent" 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_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_chainStyle="spread"
app:srcCompat="@drawable/ic_settings_24dp" app:srcCompat="@drawable/ic_settings_24dp"

View File

@ -43,6 +43,13 @@
android:entries="@array/zoom_start" android:entries="@array/zoom_start"
app:title="@string/pref_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 <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

@ -15,4 +15,10 @@
<item>@string/dropped</item> <item>@string/dropped</item>
<item>@string/repeating</item> <item>@string/repeating</item>
</string-array> </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> </resources>

View File

@ -279,6 +279,15 @@
<!-- Auto Webtoon Mode --> <!-- Auto Webtoon Mode -->
<string name="eh_auto_webtoon_snack">Reading webtoon style</string> <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 Page -->
<!-- Manga Info --> <!-- Manga Info -->
<string name="az_recommends">See Recommendations</string> <string name="az_recommends">See Recommendations</string>