mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 20:01:53 +01:00
Double pages (#699)
* Start of dual pages * Fixes to dual pages + added button to shift Next to add a button to get out of this nightmare * Adding preference to truly support on and off * Set double pages correctly for L2R reading mode * Hide shift double page in single page mode also fix shift into or out of double pages not going to right page * Shift page back on loading a new chapter * Fix back shift when reaching near end of chapter with shifted pages * Fix chapters not being marked as read when last pages are doubled * more fixes * change menu icon and verbage * Fixed going to previous/next chapter when turning on/off double shift sometimes * Fix shift icon for double page in R2L * Adding double page as option in reader sheet * Adding saving/share/setting cover for second page * Add back page indicator * RTL language support for current page in double mode * Changing to double page always moves the current page to first slot Also fix bad snap back from single to double or vice versa * Reset shift icon when going to a new chapter * Add Double pages to reader settings * Add padding to current/total pages text since it gets pretty long with double digits in double pages mode * Option for automatic single/double layout based on orientation With option to change mid read * More fixes * Lotta reworking to make this work Lotta reworking to make this work Seriously a lot of reworking to make this work * Cleanup + Some documentation Because I won't "know my own code" I genuinely do not know what I just coded * Swapped around icons for double page in chapter sheet bar Since it was confusing even me * Fix shifting not updating the ui, nor shifting back to the right page * Fix changing chapters * Fixed Prev Chapter transition page jumping back 1 * Fixed progress spinner for single page loading * Fix the case of a second page shifting for full width while double shift is on It would unshift the pages in this case, requiring another shift back If this sounds confusing or complex, it's because it is Also clean up * Update Initial dual page bg to match reader bg theme * More cleanup * More cleanup + docs + refactoring * String update * more cleanup * Update toolbar ripple based if the menu item is visible * Fix more issues with shifting + next chapter's first page being a double * Cleanup ChapterTransision Checks * Always run shifting logic * Even more optimizing which hopefully didnt break something (prolly did) (spoilers it did I had to undo this local commit) (more spoilers it still will break again) * Fix Retry in double mode * Set gifs as fullpages * Fix going from automatic to single/double page layout * More cleanup, more comments * Fix changing page layout when on a transition page for a manga with a single chapter The edge cases you can find....man * Restore shifted pages on activity recrreation * Adding a "beta" tag to page layout IMO it's absolutely solid and release candidate ready, but you can get away with so much by calling something a "beta" or an "alpha"
This commit is contained in:
parent
4e4f9b339f
commit
6d6766a86a
@ -65,6 +65,8 @@ object PreferenceKeys {
|
||||
|
||||
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
|
||||
|
||||
const val pageLayout = "page_layout"
|
||||
|
||||
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
||||
const val showNavigationOverlayNewUserWebtoon = "reader_navigation_overlay_new_user_webtoon"
|
||||
|
||||
|
@ -11,6 +11,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.io.File
|
||||
@ -143,6 +144,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, ViewerNavigation.TappingInvertMode.NONE)
|
||||
|
||||
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PageLayout.AUTOMATIC)
|
||||
|
||||
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
||||
|
||||
fun showNavigationOverlayNewUserWebtoon() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUserWebtoon, true)
|
||||
|
@ -8,10 +8,12 @@ import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -20,9 +22,10 @@ import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
@ -46,6 +49,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||
@ -57,6 +62,7 @@ import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.hasSideNavBar
|
||||
import eu.kanade.tachiyomi.util.system.isBottomTappable
|
||||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
@ -160,6 +166,10 @@ class ReaderActivity :
|
||||
|
||||
var isLoading = false
|
||||
|
||||
var lastShiftDoubleState: Boolean? = null
|
||||
var indexPageToShift: Int? = null
|
||||
var indexChapterToShift: Long? = null
|
||||
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
const val LEFT_TO_RIGHT = 1
|
||||
@ -168,6 +178,10 @@ class ReaderActivity :
|
||||
const val WEBTOON = 4
|
||||
const val VERTICAL_PLUS = 5
|
||||
|
||||
const val SHIFT_DOUBLE_PAGES = "shiftingDoublePages"
|
||||
const val SHIFTED_PAGE_INDEX = "shiftedPageIndex"
|
||||
const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
||||
val intent = Intent(context, ReaderActivity::class.java)
|
||||
intent.putExtra("manga", manga.id)
|
||||
@ -217,6 +231,9 @@ class ReaderActivity :
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
menuVisible = savedInstanceState.getBoolean(::menuVisible.name)
|
||||
lastShiftDoubleState = savedInstanceState.get(SHIFT_DOUBLE_PAGES) as? Boolean
|
||||
indexPageToShift = savedInstanceState.get(SHIFTED_PAGE_INDEX) as? Int
|
||||
indexChapterToShift = savedInstanceState.get(SHIFTED_CHAP_INDEX) as? Long
|
||||
binding.readerNav.root.isVisible = menuVisible
|
||||
} else {
|
||||
binding.readerNav.root.gone()
|
||||
@ -251,6 +268,16 @@ class ReaderActivity :
|
||||
*/
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(::menuVisible.name, menuVisible)
|
||||
(viewer as? PagerViewer)?.let { pViewer ->
|
||||
val config = pViewer.config
|
||||
outState.putBoolean(SHIFT_DOUBLE_PAGES, config.shiftDoublePage)
|
||||
if (config.shiftDoublePage) {
|
||||
pViewer.getShiftedPage()?.let {
|
||||
outState.putInt(SHIFTED_PAGE_INDEX, it.index)
|
||||
outState.putLong(SHIFTED_CHAP_INDEX, it.chapter.chapter.id ?: 0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isChangingConfigurations) {
|
||||
presenter.onSaveInstanceStateNonConfigurationChange()
|
||||
}
|
||||
@ -279,6 +306,65 @@ class ReaderActivity :
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
val splitItem = menu?.findItem(R.id.action_shift_double_page)
|
||||
splitItem?.isVisible = (viewer as? PagerViewer)?.config?.doublePages ?: false
|
||||
(viewer as? PagerViewer)?.config?.let { config ->
|
||||
splitItem?.icon = ContextCompat.getDrawable(
|
||||
this,
|
||||
if ((!config.shiftDoublePage).xor(viewer is R2LPagerViewer)) R.drawable.ic_page_previous_outline_24dp else R.drawable.ic_page_next_outline_24dp
|
||||
)
|
||||
}
|
||||
setBottomNavButtons(preferences.pageLayout().get())
|
||||
(binding.toolbar.background as? LayerDrawable)?.let { layerDrawable ->
|
||||
val isDoublePage = splitItem?.isVisible ?: false
|
||||
// Shout out to Google for not fixing setVisible https://issuetracker.google.com/issues/127538945
|
||||
layerDrawable.findDrawableByLayerId(R.id.layer_full_width).alpha = if (!isDoublePage) 255 else 0
|
||||
layerDrawable.findDrawableByLayerId(R.id.layer_one_item).alpha = if (isDoublePage) 255 else 0
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
fun setBottomNavButtons(pageLayout: Int) {
|
||||
val isDoublePage = pageLayout == PageLayout.DOUBLE_PAGES ||
|
||||
(pageLayout == PageLayout.AUTOMATIC && (viewer as? PagerViewer)?.config?.doublePages ?: false)
|
||||
binding.chaptersSheet.doublePage.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
this,
|
||||
if (!isDoublePage) R.drawable.ic_single_page_24dp
|
||||
else R.drawable.ic_book_open_variant_24dp
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
binding.chaptersSheet.doublePage.tooltipText =
|
||||
getString(
|
||||
if (isDoublePage) R.string.switch_to_single
|
||||
else R.string.switch_to_double
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the options menu was clicked. Used to handle clicks on our menu
|
||||
* entries.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_shift_double_page -> {
|
||||
(viewer as? PagerViewer)?.config?.let { config ->
|
||||
config.shiftDoublePage = !config.shiftDoublePage
|
||||
presenter.viewerChapters?.let {
|
||||
(viewer as? PagerViewer)?.updateShifting()
|
||||
(viewer as? PagerViewer)?.setChaptersDoubleShift(it)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun popToMain() {
|
||||
presenter.onBackPressed()
|
||||
if (fromUrl) {
|
||||
@ -358,6 +444,16 @@ class ReaderActivity :
|
||||
}
|
||||
}
|
||||
|
||||
binding.chaptersSheet.doublePage.setOnClickListener {
|
||||
if (preferences.pageLayout().get() == PageLayout.AUTOMATIC) {
|
||||
(viewer as? PagerViewer)?.config?.let { config ->
|
||||
config.doublePages = !config.doublePages
|
||||
reloadChapters(config.doublePages, true)
|
||||
}
|
||||
} else {
|
||||
preferences.pageLayout().set(1 - preferences.pageLayout().get())
|
||||
}
|
||||
}
|
||||
binding.readerNav.leftChapter.setOnClickListener {
|
||||
if (isLoading) {
|
||||
return@setOnClickListener
|
||||
@ -579,6 +675,7 @@ class ReaderActivity :
|
||||
binding.viewerContainer.removeAllViews()
|
||||
}
|
||||
viewer = newViewer
|
||||
binding.chaptersSheet.doublePage.isVisible = viewer is PagerViewer
|
||||
binding.viewerContainer.addView(newViewer.getView())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@ -591,6 +688,14 @@ class ReaderActivity :
|
||||
}
|
||||
}
|
||||
|
||||
if (newViewer is PagerViewer) {
|
||||
if (preferences.pageLayout().get() == PageLayout.AUTOMATIC) {
|
||||
setDoublePageMode(newViewer)
|
||||
}
|
||||
lastShiftDoubleState?.let { newViewer.config.shiftDoublePage = it }
|
||||
lastShiftDoubleState = null
|
||||
}
|
||||
|
||||
binding.navigationOverlay.isLTR = !(viewer is L2RPagerViewer)
|
||||
binding.viewerContainer.setBackgroundColor(
|
||||
if (viewer is WebtoonViewer) {
|
||||
@ -606,6 +711,7 @@ class ReaderActivity :
|
||||
|
||||
binding.pleaseWait.visible()
|
||||
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -613,12 +719,44 @@ class ReaderActivity :
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun reloadChapters(doublePages: Boolean, force: Boolean = false) {
|
||||
val pViewer = viewer as? PagerViewer ?: return
|
||||
pViewer.updateShifting()
|
||||
if (!force && pViewer.config.autoDoublePages) {
|
||||
setDoublePageMode(pViewer)
|
||||
} else {
|
||||
pViewer.config.doublePages = doublePages
|
||||
}
|
||||
val currentChapter = presenter.getCurrentChapter()
|
||||
if (doublePages) {
|
||||
// If we're moving from singe to double, we want the current page to be the first page
|
||||
pViewer.config.shiftDoublePage = (
|
||||
binding.readerNav.pageSeekbar.progress +
|
||||
(
|
||||
currentChapter?.pages?.subList(0, binding.readerNav.pageSeekbar.progress)
|
||||
?.count { it.fullPage || it.isolatedPage } ?: 0
|
||||
)
|
||||
) % 2 != 0
|
||||
}
|
||||
presenter.viewerChapters?.let {
|
||||
pViewer.setChaptersDoubleShift(it)
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter whenever a new [viewerChapters] have been set. It delegates the
|
||||
* method to the current viewer, but also set the subtitle on the binding.toolbar.
|
||||
*/
|
||||
fun setChapters(viewerChapters: ViewerChapters) {
|
||||
binding.pleaseWait.gone()
|
||||
if (indexChapterToShift != null && indexPageToShift != null) {
|
||||
viewerChapters.currChapter.pages?.find { it.index == indexPageToShift && it.chapter.chapter.id == indexChapterToShift }?.let {
|
||||
(viewer as? PagerViewer)?.updateShifting(it)
|
||||
}
|
||||
indexChapterToShift = null
|
||||
indexPageToShift = null
|
||||
}
|
||||
viewer?.setChapters(viewerChapters)
|
||||
intentPageNumber?.let { moveToPageIndex(it) }
|
||||
intentPageNumber = null
|
||||
@ -687,58 +825,100 @@ class ReaderActivity :
|
||||
* bottom menu and delegates the change to the presenter.
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun onPageSelected(page: ReaderPage) {
|
||||
presenter.onPageSelected(page)
|
||||
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
|
||||
presenter.onPageSelected(page, hasExtraPage)
|
||||
val pages = page.chapter.pages ?: return
|
||||
|
||||
val currentPage = page.number
|
||||
val totalPages = pages.size
|
||||
|
||||
// Set bottom page number
|
||||
binding.pageNumber.text = "$currentPage/$totalPages"
|
||||
|
||||
if (viewer is R2LPagerViewer) {
|
||||
binding.readerNav.rightPageText.text = currentPage.toString()
|
||||
binding.readerNav.leftPageText.text = totalPages.toString()
|
||||
val currentPage = if (hasExtraPage) {
|
||||
if (resources.isLTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}"
|
||||
} else {
|
||||
binding.readerNav.leftPageText.text = currentPage.toString()
|
||||
binding.readerNav.rightPageText.text = totalPages.toString()
|
||||
"${page.number}"
|
||||
}
|
||||
|
||||
val totalPages = pages.size.toString()
|
||||
binding.pageNumber.text = if (resources.isLTR) "$currentPage/$totalPages" else "$totalPages/$currentPage"
|
||||
if (viewer is R2LPagerViewer) {
|
||||
binding.readerNav.rightPageText.text = currentPage
|
||||
binding.readerNav.leftPageText.text = totalPages
|
||||
} else {
|
||||
binding.readerNav.leftPageText.text = currentPage
|
||||
binding.readerNav.rightPageText.text = totalPages
|
||||
}
|
||||
if (binding.chaptersSheet.chaptersBottomSheet.selectedChapterId != page.chapter.chapter.id) {
|
||||
binding.chaptersSheet.chaptersBottomSheet.refreshList()
|
||||
}
|
||||
// Set seekbar progress
|
||||
binding.readerNav.pageSeekbar.max = pages.lastIndex
|
||||
binding.readerNav.pageSeekbar.progress = page.index
|
||||
val progress = page.index + if (hasExtraPage) 1 else 0
|
||||
// For a double page, show the last 2 pages as if it was the final part of the seekbar
|
||||
binding.readerNav.pageSeekbar.progress = if (progress == pages.lastIndex) progress else page.index
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the viewer whenever a [page] is long clicked. A bottom sheet with a list of
|
||||
* actions to perform is shown.
|
||||
*/
|
||||
fun onPageLongTap(page: ReaderPage) {
|
||||
val items = listOf(
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
0,
|
||||
R.drawable.ic_share_24dp,
|
||||
R.string.share
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
1,
|
||||
R.drawable.ic_save_24dp,
|
||||
R.string.save
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
2,
|
||||
R.drawable.ic_photo_24dp,
|
||||
R.string.set_as_cover
|
||||
fun onPageLongTap(page: ReaderPage, extraPage: ReaderPage? = null) {
|
||||
val items = if (extraPage != null) {
|
||||
listOf(
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
3,
|
||||
R.drawable.ic_outline_share_24dp,
|
||||
R.string.share_second_page
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
4,
|
||||
R.drawable.ic_outline_save_24dp,
|
||||
R.string.save_second_page
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
5,
|
||||
R.drawable.ic_outline_photo_24dp,
|
||||
R.string.set_second_page_as_cover
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
0,
|
||||
R.drawable.ic_share_24dp,
|
||||
R.string.share_first_page
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
1,
|
||||
R.drawable.ic_save_24dp,
|
||||
R.string.save_first_page
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
2,
|
||||
R.drawable.ic_photo_24dp,
|
||||
R.string.set_first_page_as_cover
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
0,
|
||||
R.drawable.ic_share_24dp,
|
||||
R.string.share
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
1,
|
||||
R.drawable.ic_save_24dp,
|
||||
R.string.save
|
||||
),
|
||||
MaterialMenuSheet.MenuSheetItem(
|
||||
2,
|
||||
R.drawable.ic_photo_24dp,
|
||||
R.string.set_as_cover
|
||||
)
|
||||
)
|
||||
}
|
||||
MaterialMenuSheet(this, items) { _, item ->
|
||||
when (item) {
|
||||
0 -> shareImage(page)
|
||||
1 -> saveImage(page)
|
||||
2 -> showSetCoverPrompt(page)
|
||||
3 -> extraPage?.let { shareImage(it) }
|
||||
4 -> extraPage?.let { saveImage(it) }
|
||||
5 -> extraPage?.let { showSetCoverPrompt(it) }
|
||||
}
|
||||
true
|
||||
}.show()
|
||||
@ -911,6 +1091,11 @@ class ReaderActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDoublePageMode(viewer: PagerViewer) {
|
||||
val currentOrientation = resources.configuration.orientation
|
||||
viewer.config.doublePages = (currentOrientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
private fun handleIntentAction(intent: Intent): Boolean {
|
||||
val uri = intent.data ?: return false
|
||||
if (!presenter.canLoadUrl(uri)) {
|
||||
@ -1000,6 +1185,12 @@ class ReaderActivity :
|
||||
preferences.alwaysShowChapterTransition().asFlow()
|
||||
.onEach { showNewChapter = it }
|
||||
.launchIn(scope)
|
||||
|
||||
preferences.pageLayout().asFlow()
|
||||
.onEach {
|
||||
setBottomNavButtons(it)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,6 +86,9 @@ class ReaderPresenter(
|
||||
*/
|
||||
private val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
|
||||
|
||||
val viewerChapters: ViewerChapters?
|
||||
get() = viewerChaptersRelay.value
|
||||
|
||||
/**
|
||||
* Relay used when loading prev/next chapter needed to lock the UI (with a dialog).
|
||||
*/
|
||||
@ -398,8 +401,7 @@ class ReaderPresenter(
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst(
|
||||
{ view, _ ->
|
||||
val lastPage = if (chapter.pages_left <= 1) 0 else chapter.last_page_read
|
||||
view.moveToPageIndex(lastPage)
|
||||
view.moveToPageIndex(0)
|
||||
view.refreshChapters()
|
||||
},
|
||||
{ _, _ ->
|
||||
@ -458,7 +460,7 @@ class ReaderPresenter(
|
||||
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||
* [page]'s chapter is different from the currently active.
|
||||
*/
|
||||
fun onPageSelected(page: ReaderPage) {
|
||||
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
|
||||
val currentChapters = viewerChaptersRelay.value ?: return
|
||||
|
||||
val selectedChapter = page.chapter
|
||||
@ -467,7 +469,10 @@ class ReaderPresenter(
|
||||
selectedChapter.chapter.last_page_read = page.index
|
||||
selectedChapter.chapter.pages_left =
|
||||
(selectedChapter.pages?.size ?: page.index) - page.index
|
||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||
// For double pages, check if the second to last page is doubled up
|
||||
if (selectedChapter.pages?.lastIndex == page.index ||
|
||||
(hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index)
|
||||
) {
|
||||
selectedChapter.chapter.read = true
|
||||
updateTrackChapterRead(selectedChapter)
|
||||
deleteChapterIfNeeded(selectedChapter)
|
||||
|
@ -17,8 +17,9 @@ sealed class ChapterTransition {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ChapterTransition) return false
|
||||
if (from == other.from && to == other.to) return true
|
||||
if (from == other.to && to == other.from) return true
|
||||
if (from == other.from && to == other.to && to != null) return true
|
||||
if (from == other.to && to == other.from && to != null) return true
|
||||
if (to == other.to && to == null && from == other.from && other::class == this::class) return true
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,19 @@ class ReaderPage(
|
||||
imageUrl: String? = null,
|
||||
var stream: (() -> InputStream)? = null,
|
||||
var bg: Drawable? = null,
|
||||
var bgType: Int? = null
|
||||
var bgType: Int? = null,
|
||||
/** Value to check if this page is used to as if it was too wide */
|
||||
var shiftedPage: Boolean = false,
|
||||
/** Value to check if a page is can be doubled up, but can't because the next page is too wide */
|
||||
var isolatedPage: Boolean = false
|
||||
) : Page(index, url, imageUrl, null) {
|
||||
|
||||
lateinit var chapter: ReaderChapter
|
||||
|
||||
/** Value to check if a page is too wide to be doubled up */
|
||||
var fullPage: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) shiftedPage = false
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.bindToPreference
|
||||
import eu.kanade.tachiyomi.util.lang.addBetaTag
|
||||
import eu.kanade.tachiyomi.util.view.visibleIf
|
||||
import eu.kanade.tachiyomi.widget.BaseReaderSettingsView
|
||||
|
||||
@ -27,12 +28,18 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
binding.pagerNav.bindToPreference(preferences.navigationModePager())
|
||||
binding.pagerInvert.bindToPreference(preferences.pagerNavInverted())
|
||||
binding.extendPastCutout.bindToPreference(preferences.pagerCutoutBehavior())
|
||||
binding.pageLayout.bindToPreference(preferences.pageLayout())
|
||||
|
||||
binding.pageLayout.title = binding.pageLayout.title.toString().addBetaTag(context)
|
||||
|
||||
val mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaViewer() ?: 0
|
||||
val isWebtoonView = mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS
|
||||
val hasMargins = mangaViewer == ReaderActivity.VERTICAL_PLUS
|
||||
binding.cropBordersWebtoon.bindToPreference(if (hasMargins) preferences.cropBorders() else preferences.cropBordersWebtoon())
|
||||
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
||||
binding.webtoonSidePadding.bindToIntPreference(
|
||||
preferences.webtoonSidePadding(),
|
||||
R.array.webtoon_side_padding_values
|
||||
)
|
||||
binding.webtoonEnableZoomOut.bindToPreference(preferences.webtoonEnableZoomOut())
|
||||
binding.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
|
||||
binding.webtoonInvert.bindToPreference(preferences.webtoonNavInverted())
|
||||
@ -49,8 +56,22 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
}
|
||||
|
||||
private fun updatePagedGroup(show: Boolean) {
|
||||
listOf(binding.scaleType, binding.zoomStart, binding.cropBorders, binding.pageTransitions, binding.pagerNav, binding.pagerInvert).forEach { it.visibleIf(show) }
|
||||
listOf(binding.cropBordersWebtoon, binding.webtoonSidePadding, binding.webtoonEnableZoomOut, binding.webtoonNav, binding.webtoonInvert).forEach { it.visibleIf(!show) }
|
||||
listOf(
|
||||
binding.scaleType,
|
||||
binding.zoomStart,
|
||||
binding.cropBorders,
|
||||
binding.pageTransitions,
|
||||
binding.pagerNav,
|
||||
binding.pagerInvert,
|
||||
binding.pageLayout
|
||||
).forEach { it.visibleIf(show) }
|
||||
listOf(
|
||||
binding.cropBordersWebtoon,
|
||||
binding.webtoonSidePadding,
|
||||
binding.webtoonEnableZoomOut,
|
||||
binding.webtoonNav,
|
||||
binding.webtoonInvert
|
||||
).forEach { it.visibleIf(!show) }
|
||||
val isFullFit = when (preferences.imageScaleType().get()) {
|
||||
SubsamplingScaleImageView.SCALE_TYPE_FIT_HEIGHT,
|
||||
SubsamplingScaleImageView.SCALE_TYPE_SMART_FIT,
|
||||
|
@ -16,6 +16,7 @@ abstract class ViewerConfig(preferences: PreferencesHelper) {
|
||||
protected val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
var imagePropertyChangedListener: (() -> Unit)? = null
|
||||
var reloadChapterListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
var navigationModeChangedListener: (() -> Unit)? = null
|
||||
var navigationModeInvertedListener: (() -> Unit)? = null
|
||||
|
@ -38,6 +38,18 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
||||
var cutoutBehavior = 0
|
||||
private set
|
||||
|
||||
var shiftDoublePage = false
|
||||
|
||||
var doublePages = preferences.pageLayout().get() == PageLayout.DOUBLE_PAGES
|
||||
set(value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
shiftDoublePage = false
|
||||
}
|
||||
}
|
||||
|
||||
var autoDoublePages = preferences.pageLayout().get() == PageLayout.AUTOMATIC
|
||||
|
||||
init {
|
||||
preferences.pageTransitions()
|
||||
.register({ usePageTransitions = it })
|
||||
@ -75,6 +87,25 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
||||
preferences.readerTheme()
|
||||
.register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
preferences.pageLayout()
|
||||
.asFlow()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
autoDoublePages = it == PageLayout.AUTOMATIC
|
||||
if (!autoDoublePages) {
|
||||
doublePages = it == PageLayout.DOUBLE_PAGES
|
||||
}
|
||||
reloadChapterListener?.invoke(doublePages)
|
||||
}
|
||||
.launchIn(scope)
|
||||
preferences.pageLayout()
|
||||
.register({
|
||||
autoDoublePages = it == PageLayout.AUTOMATIC
|
||||
if (!autoDoublePages) {
|
||||
doublePages = it == PageLayout.DOUBLE_PAGES
|
||||
}
|
||||
})
|
||||
|
||||
navigationOverlayForNewUser = preferences.showNavigationOverlayNewUser().get()
|
||||
if (navigationOverlayForNewUser) {
|
||||
preferences.showNavigationOverlayNewUser().set(false)
|
||||
@ -141,3 +172,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
||||
const val CUTOUT_IGNORE = 2
|
||||
}
|
||||
}
|
||||
|
||||
object PageLayout {
|
||||
const val SINGLE_PAGE = 0
|
||||
const val DOUBLE_PAGES = 1
|
||||
const val AUTOMATIC = 2
|
||||
}
|
||||
|
@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.GestureDetector
|
||||
import android.view.Gravity
|
||||
@ -48,8 +51,11 @@ import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* View of the ViewPager that contains a page of a chapter.
|
||||
@ -57,14 +63,15 @@ import java.util.concurrent.TimeUnit
|
||||
@SuppressLint("ViewConstructor")
|
||||
class PagerPageHolder(
|
||||
val viewer: PagerViewer,
|
||||
val page: ReaderPage
|
||||
val page: ReaderPage,
|
||||
private var extraPage: ReaderPage? = null
|
||||
) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView {
|
||||
|
||||
/**
|
||||
* Item that identifies this view. Needed by the adapter to not recreate views.
|
||||
*/
|
||||
override val item
|
||||
get() = page
|
||||
get() = page to extraPage
|
||||
|
||||
/**
|
||||
* Loading progress bar to indicate the current progress.
|
||||
@ -101,12 +108,28 @@ class PagerPageHolder(
|
||||
*/
|
||||
private var progressSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
*/
|
||||
private var extraStatusSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for progress changes of the page.
|
||||
*/
|
||||
private var extraProgressSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription used to read the header of the image. This is needed in order to instantiate
|
||||
* the appropiate image view depending if the image is animated (GIF).
|
||||
*/
|
||||
private var readImageHeaderSubscription: Subscription? = null
|
||||
|
||||
var status: Int = 0
|
||||
var extraStatus: Int = 0
|
||||
var progress: Int = 0
|
||||
var extraProgress: Int = 0
|
||||
private var skipExtra = false
|
||||
|
||||
init {
|
||||
addView(progressBar)
|
||||
observeStatus()
|
||||
@ -124,8 +147,10 @@ class PagerPageHolder(
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unsubscribeProgress()
|
||||
unsubscribeStatus()
|
||||
unsubscribeProgress(1)
|
||||
unsubscribeStatus(1)
|
||||
unsubscribeProgress(2)
|
||||
unsubscribeStatus(2)
|
||||
unsubscribeReadImageHeader()
|
||||
subsamplingImageView?.setOnImageEventListener(null)
|
||||
}
|
||||
@ -141,7 +166,18 @@ class PagerPageHolder(
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
statusSubscription = loader.getPage(page)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processStatus(it) }
|
||||
.subscribe {
|
||||
status = it
|
||||
processStatus(it)
|
||||
}
|
||||
val extraPage = extraPage ?: return
|
||||
val loader2 = extraPage.chapter.pageLoader ?: return
|
||||
extraStatusSubscription = loader2.getPage(extraPage)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
extraStatus = it
|
||||
processStatus2(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,7 +191,28 @@ class PagerPageHolder(
|
||||
.distinctUntilChanged()
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { value -> progressBar.setProgress(value) }
|
||||
.subscribe { value ->
|
||||
progress = value
|
||||
if (extraPage == null) {
|
||||
progressBar.setProgress(progress)
|
||||
} else {
|
||||
progressBar.setProgress((progress + extraProgress) / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress2() {
|
||||
extraProgressSubscription?.unsubscribe()
|
||||
val extraPage = extraPage ?: return
|
||||
extraProgressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.map { extraPage.progress }
|
||||
.distinctUntilChanged()
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { value ->
|
||||
extraProgress = value
|
||||
progressBar.setProgress((progress + extraProgress) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,12 +229,40 @@ class PagerPageHolder(
|
||||
setDownloading()
|
||||
}
|
||||
Page.READY -> {
|
||||
setImage()
|
||||
unsubscribeProgress()
|
||||
if (extraStatus == Page.READY || extraPage == null) {
|
||||
setImage()
|
||||
}
|
||||
unsubscribeProgress(1)
|
||||
}
|
||||
Page.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress()
|
||||
unsubscribeProgress(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the status of the page changes.
|
||||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus2(status: Int) {
|
||||
when (status) {
|
||||
Page.QUEUE -> setQueued()
|
||||
Page.LOAD_PAGE -> setLoading()
|
||||
Page.DOWNLOAD_IMAGE -> {
|
||||
observeProgress2()
|
||||
setDownloading()
|
||||
}
|
||||
Page.READY -> {
|
||||
if (this.status == Page.READY) {
|
||||
setImage()
|
||||
}
|
||||
unsubscribeProgress(2)
|
||||
}
|
||||
Page.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,17 +270,19 @@ class PagerPageHolder(
|
||||
/**
|
||||
* Unsubscribes from the status subscription.
|
||||
*/
|
||||
private fun unsubscribeStatus() {
|
||||
statusSubscription?.unsubscribe()
|
||||
statusSubscription = null
|
||||
private fun unsubscribeStatus(page: Int) {
|
||||
val subscription = if (page == 1) statusSubscription else extraStatusSubscription
|
||||
subscription?.unsubscribe()
|
||||
if (page == 1) statusSubscription = null else extraStatusSubscription = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun unsubscribeProgress() {
|
||||
progressSubscription?.unsubscribe()
|
||||
progressSubscription = null
|
||||
private fun unsubscribeProgress(page: Int) {
|
||||
val subscription = if (page == 1) progressSubscription else extraProgressSubscription
|
||||
subscription?.unsubscribe()
|
||||
if (page == 1) progressSubscription = null else extraProgressSubscription = null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,23 +331,30 @@ class PagerPageHolder(
|
||||
|
||||
unsubscribeReadImageHeader()
|
||||
val streamFn = page.stream ?: return
|
||||
val streamFn2 = extraPage?.stream
|
||||
|
||||
var openStream: InputStream? = null
|
||||
|
||||
readImageHeaderSubscription = Observable
|
||||
.fromCallable {
|
||||
val stream = streamFn().buffered(16)
|
||||
openStream = stream
|
||||
|
||||
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
|
||||
val stream2 = if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null
|
||||
openStream = this@PagerPageHolder.mergePages(stream, stream2)
|
||||
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF ||
|
||||
if (stream2 != null) ImageUtil.findImageType(stream2) == ImageUtil.ImageType.GIF else false
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { isAnimated ->
|
||||
if (skipExtra) {
|
||||
onPageSplit()
|
||||
}
|
||||
if (!isAnimated) {
|
||||
if (viewer.config.readerTheme >= 2) {
|
||||
val imageView = initSubsamplingImageView()
|
||||
if (page.bg != null &&
|
||||
page.bgType == getBGType(viewer.config.readerTheme, context)
|
||||
page.bgType == getBGType(viewer.config.readerTheme, context) + item.hashCode()
|
||||
) {
|
||||
imageView.setImage(ImageSource.inputStream(openStream!!))
|
||||
imageView.background = page.bg
|
||||
@ -275,7 +369,7 @@ class PagerPageHolder(
|
||||
launchUI {
|
||||
imageView.background = setBG(bytesArray)
|
||||
page.bg = imageView.background
|
||||
page.bgType = getBGType(viewer.config.readerTheme, context)
|
||||
page.bgType = getBGType(viewer.config.readerTheme, context) + item.hashCode()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -293,7 +387,9 @@ class PagerPageHolder(
|
||||
// Keep the Rx stream alive to close the input stream only when unsubscribed
|
||||
.flatMap { Observable.never<Unit>() }
|
||||
.doOnUnsubscribe {
|
||||
try { openStream?.close() } catch (e: Exception) {}
|
||||
try {
|
||||
openStream?.close()
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
.subscribe({}, {})
|
||||
}
|
||||
@ -468,6 +564,9 @@ class PagerPageHolder(
|
||||
setText(R.string.retry)
|
||||
setOnClickListener {
|
||||
page.chapter.pageLoader?.retryPage(page)
|
||||
extraPage?.let {
|
||||
it.chapter.pageLoader?.retryPage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
addView(retryButton)
|
||||
@ -530,6 +629,83 @@ class PagerPageHolder(
|
||||
return decodeLayout
|
||||
}
|
||||
|
||||
private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream {
|
||||
imageStream2 ?: return imageStream
|
||||
if (page.fullPage) return imageStream
|
||||
if (ImageUtil.findImageType(imageStream) == ImageUtil.ImageType.GIF) {
|
||||
page.fullPage = true
|
||||
skipExtra = true
|
||||
return imageStream
|
||||
} else if (ImageUtil.findImageType(imageStream2) == ImageUtil.ImageType.GIF) {
|
||||
page.isolatedPage = true
|
||||
extraPage?.fullPage = true
|
||||
skipExtra = true
|
||||
return imageStream
|
||||
}
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
if (height < width) {
|
||||
imageStream2.close()
|
||||
imageStream.close()
|
||||
page.fullPage = true
|
||||
skipExtra = true
|
||||
return imageBytes.inputStream()
|
||||
}
|
||||
|
||||
val imageBytes2 = imageStream2.readBytes()
|
||||
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
||||
val height2 = imageBitmap2.height
|
||||
val width2 = imageBitmap2.width
|
||||
|
||||
if (height2 < width2) {
|
||||
imageStream2.close()
|
||||
imageStream.close()
|
||||
extraPage?.fullPage = true
|
||||
page.isolatedPage = true
|
||||
skipExtra = true
|
||||
return imageBytes.inputStream()
|
||||
}
|
||||
|
||||
val maxHeight = max(height, height2)
|
||||
|
||||
val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
canvas.drawColor(if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) Color.WHITE else Color.BLACK)
|
||||
val isLTR = viewer !is R2LPagerViewer
|
||||
val upperPart = Rect(
|
||||
if (isLTR) 0 else width2,
|
||||
(maxHeight - imageBitmap.height) / 2,
|
||||
(if (isLTR) 0 else width2) + imageBitmap.width,
|
||||
imageBitmap.height + (maxHeight - imageBitmap.height) / 2
|
||||
)
|
||||
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
|
||||
val bottomPart = Rect(
|
||||
if (!isLTR) 0 else width,
|
||||
(maxHeight - imageBitmap2.height) / 2,
|
||||
(if (!isLTR) 0 else width) + imageBitmap2.width,
|
||||
imageBitmap2.height + (maxHeight - imageBitmap2.height) / 2
|
||||
)
|
||||
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
imageStream.close()
|
||||
imageStream2.close()
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
private fun onPageSplit() {
|
||||
extraPage ?: return
|
||||
viewer.onPageSplit(page)
|
||||
if (extraPage?.fullPage == true) {
|
||||
extraPage = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension method to set a [stream] into this ImageView.
|
||||
*/
|
||||
@ -541,6 +717,9 @@ class PagerPageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
private val Bitmap.rect: Rect
|
||||
get() = Rect(0, 0, width, height)
|
||||
|
||||
companion object {
|
||||
fun getBGType(readerTheme: Int, context: Context): Int {
|
||||
return if (readerTheme == 3) {
|
||||
|
@ -60,29 +60,29 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
field = value
|
||||
if (value) {
|
||||
awaitingIdleViewerChapters?.let {
|
||||
setChaptersInternal(it)
|
||||
setChaptersDoubleShift(it)
|
||||
awaitingIdleViewerChapters = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pagerListener = object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
onPageChange(position)
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isIdle = state == ViewPager.SCROLL_STATE_IDLE
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
pager.gone() // Don't layout the pager yet
|
||||
pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
pager.offscreenPageLimit = 1
|
||||
pager.id = R.id.reader_pager
|
||||
pager.adapter = adapter
|
||||
pager.addOnPageChangeListener(
|
||||
object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
onPageChange(position)
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isIdle = state == ViewPager.SCROLL_STATE_IDLE
|
||||
}
|
||||
}
|
||||
)
|
||||
pager.addOnPageChangeListener(pagerListener)
|
||||
pager.tapListener = f@{ event ->
|
||||
if (!config.tappingEnabled) {
|
||||
activity.toggleMenu()
|
||||
@ -101,9 +101,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
}
|
||||
pager.longTapListener = f@{
|
||||
if (activity.menuVisible || config.longTapEnabled) {
|
||||
val item = adapter.items.getOrNull(pager.currentItem)
|
||||
if (item is ReaderPage) {
|
||||
activity.onPageLongTap(item)
|
||||
val item = adapter.joinedItems.getOrNull(pager.currentItem)
|
||||
val firstPage = item?.first as? ReaderPage
|
||||
val secondPage = item?.second as? ReaderPage
|
||||
if (firstPage is ReaderPage) {
|
||||
activity.onPageLongTap(firstPage, secondPage)
|
||||
return@f true
|
||||
}
|
||||
}
|
||||
@ -114,6 +116,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
refreshAdapter()
|
||||
}
|
||||
|
||||
config.reloadChapterListener = {
|
||||
activity.reloadChapters(it)
|
||||
}
|
||||
|
||||
config.navigationModeChangedListener = {
|
||||
val showOnStart = config.navigationOverlayForNewUser
|
||||
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
||||
@ -136,14 +142,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
/**
|
||||
* Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
|
||||
*/
|
||||
private fun onPageChange(position: Int) {
|
||||
val page = adapter.items.getOrNull(position)
|
||||
fun onPageChange(position: Int) {
|
||||
val page = adapter.joinedItems.getOrNull(position)
|
||||
if (page != null && currentPage != page) {
|
||||
val allowPreload = checkAllowPreload(page as? ReaderPage)
|
||||
currentPage = page
|
||||
when (page) {
|
||||
is ReaderPage -> onReaderPageSelected(page, allowPreload)
|
||||
is ChapterTransition -> onTransitionSelected(page)
|
||||
val allowPreload = checkAllowPreload(page.first as? ReaderPage)
|
||||
currentPage = page.first
|
||||
when (val aPage = page.first) {
|
||||
is ReaderPage -> onReaderPageSelected(aPage, allowPreload, page.second != null)
|
||||
is ChapterTransition -> onTransitionSelected(aPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,11 +177,16 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
* Called when a [ReaderPage] is marked as active. It notifies the
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
activity.onPageSelected(page)
|
||||
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, hasExtraPage: Boolean) {
|
||||
activity.onPageSelected(page, hasExtraPage)
|
||||
|
||||
val offset = if (hasExtraPage) 1 else 0
|
||||
val pages = page.chapter.pages ?: return
|
||||
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||
if (hasExtraPage) {
|
||||
Timber.d("onReaderPageSelected: ${page.number}-${page.number + offset}/${pages.size}")
|
||||
} else {
|
||||
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||
}
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) {
|
||||
@ -202,13 +213,29 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
}
|
||||
}
|
||||
|
||||
fun setChaptersDoubleShift(chapters: ViewerChapters) {
|
||||
// Remove Listener since we're about to change the size of the items
|
||||
// If we don't the size change could put us on a new chapter
|
||||
pager.removeOnPageChangeListener(pagerListener)
|
||||
setChaptersInternal(chapters)
|
||||
pager.addOnPageChangeListener(pagerListener)
|
||||
// Since we removed the listener while shifting, call page change to update the ui
|
||||
onPageChange(pager.currentItem)
|
||||
}
|
||||
|
||||
fun updateShifting(page: ReaderPage? = null) {
|
||||
adapter.pageToShift = page ?: adapter.joinedItems[pager.currentItem].first as? ReaderPage
|
||||
}
|
||||
|
||||
fun getShiftedPage(): ReaderPage? = adapter.pageToShift
|
||||
|
||||
/**
|
||||
* Tells this viewer to set the given [chapters] as active. If the pager is currently idle,
|
||||
* it sets the chapters immediately, otherwise they are saved and set when it becomes idle.
|
||||
*/
|
||||
override fun setChapters(chapters: ViewerChapters) {
|
||||
if (isIdle) {
|
||||
setChaptersInternal(chapters)
|
||||
setChaptersDoubleShift(chapters)
|
||||
} else {
|
||||
awaitingIdleViewerChapters = chapters
|
||||
}
|
||||
@ -219,7 +246,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
*/
|
||||
private fun setChaptersInternal(chapters: ViewerChapters) {
|
||||
Timber.d("setChaptersInternal")
|
||||
val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull(
|
||||
val forceTransition = config.alwaysShowChapterTransition || adapter.joinedItems.getOrNull(
|
||||
pager
|
||||
.currentItem
|
||||
) is ChapterTransition
|
||||
@ -232,6 +259,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
moveToPage(pages[chapters.currChapter.requestedPage])
|
||||
pager.visible()
|
||||
}
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -239,13 +267,21 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
*/
|
||||
override fun moveToPage(page: ReaderPage) {
|
||||
Timber.d("moveToPage ${page.number}")
|
||||
val position = adapter.items.indexOf(page)
|
||||
val position = adapter.joinedItems.indexOfFirst { it.first == page || it.second == page }
|
||||
if (position != -1) {
|
||||
val currentPosition = pager.currentItem
|
||||
pager.setCurrentItem(position, true)
|
||||
// manually call onPageChange since ViewPager listener is not triggered in this case
|
||||
if (currentPosition == position) {
|
||||
onPageChange(position)
|
||||
} else {
|
||||
// Call this since with double shift onPageChange wont get called (it shouldn't)
|
||||
// Instead just update the page count in ui
|
||||
val joinedItem = adapter.joinedItems.firstOrNull { it.first == page || it.second == page }
|
||||
activity.onPageSelected(
|
||||
joinedItem?.first as? ReaderPage ?: page,
|
||||
joinedItem?.second != null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.d("Page $page not found in adapter")
|
||||
@ -342,6 +378,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
return true
|
||||
}
|
||||
|
||||
fun onPageSplit(currentPage: ReaderPage) {
|
||||
adapter.onPageSplit(currentPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the containing activity when a generic motion [event] is received. It should
|
||||
* return true if the event was handled, false otherwise.
|
||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||
@ -15,14 +16,24 @@ import timber.log.Timber
|
||||
class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* List of currently set items.
|
||||
* Paired list of currently set items.
|
||||
*/
|
||||
var items: List<Any> = emptyList()
|
||||
var joinedItems: MutableList<Pair<Any, Any?>> = mutableListOf()
|
||||
private set
|
||||
|
||||
/** Single list of items */
|
||||
private var subItems: MutableList<Any> = mutableListOf()
|
||||
|
||||
var nextTransition: ChapterTransition.Next? = null
|
||||
private set
|
||||
|
||||
/** Page used to start the shifted pages */
|
||||
var pageToShift: ReaderPage? = null
|
||||
|
||||
/** Varibles used to check if config of the pages have changed */
|
||||
private var shifted = viewer.config.shiftDoublePage
|
||||
private var doubledUp = viewer.config.doublePages
|
||||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
|
||||
/**
|
||||
@ -38,8 +49,15 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
||||
// selected as the current chapter when one of those pages is selected.
|
||||
val prevPages = chapters.prevChapter.pages
|
||||
// We will take an even number of pages if the page count if even
|
||||
// however we should take account full pages when deciding
|
||||
val numberOfFullPages =
|
||||
(
|
||||
chapters.prevChapter.pages?.count { it.fullPage || it.isolatedPage }
|
||||
?: 0
|
||||
)
|
||||
if (prevPages != null) {
|
||||
newItems.addAll(prevPages.takeLast(2))
|
||||
newItems.addAll(prevPages.takeLast(if ((prevPages.size + numberOfFullPages) % 2 == 0) 2 else 3))
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,27 +93,34 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
if (viewer is R2LPagerViewer) {
|
||||
newItems.reverse()
|
||||
}
|
||||
subItems = newItems.toMutableList()
|
||||
|
||||
items = newItems
|
||||
notifyDataSetChanged()
|
||||
var useSecondPage = false
|
||||
if (shifted != viewer.config.shiftDoublePage || (doubledUp != viewer.config.doublePages && doubledUp)) {
|
||||
if (shifted && (doubledUp == viewer.config.doublePages)) {
|
||||
useSecondPage = true
|
||||
}
|
||||
shifted = viewer.config.shiftDoublePage
|
||||
}
|
||||
doubledUp = viewer.config.doublePages
|
||||
setJoinedItems(useSecondPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of items of the adapter.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
return joinedItems.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view for the item at the given [position].
|
||||
*/
|
||||
override fun createView(container: ViewGroup, position: Int): View {
|
||||
return when (val item = items[position]) {
|
||||
is ReaderPage -> PagerPageHolder(viewer, item)
|
||||
val item = joinedItems[position].first
|
||||
val item2 = joinedItems[position].second
|
||||
return when (item) {
|
||||
is ReaderPage -> PagerPageHolder(viewer, item, item2 as? ReaderPage)
|
||||
is ChapterTransition -> PagerTransitionHolder(viewer, item)
|
||||
else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented")
|
||||
}
|
||||
@ -106,7 +131,9 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
*/
|
||||
override fun getItemPosition(view: Any): Int {
|
||||
if (view is PositionableView) {
|
||||
val position = items.indexOf(view.item)
|
||||
val position = joinedItems.indexOfFirst {
|
||||
view.item == (it.first to it.second)
|
||||
}
|
||||
if (position != -1) {
|
||||
return position
|
||||
} else {
|
||||
@ -115,4 +142,143 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
return POSITION_NONE
|
||||
}
|
||||
|
||||
fun onPageSplit(current: ReaderPage) {
|
||||
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
||||
setJoinedItems(
|
||||
oldCurrent?.second == current ||
|
||||
(current.index + 1) < (
|
||||
(
|
||||
oldCurrent?.second
|
||||
?: oldCurrent?.first
|
||||
) as? ReaderPage
|
||||
)?.index ?: 0
|
||||
)
|
||||
|
||||
// The listener may be removed when we split a page, so the ui may not have updated properly
|
||||
// This case usually happens when we load a new chapter and the first 2 pages need to split og
|
||||
viewer.pager.post {
|
||||
viewer.onPageChange(viewer.pager.currentItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setJoinedItems(useSecondPage: Boolean = false) {
|
||||
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
|
||||
if (!viewer.config.doublePages) {
|
||||
// If not in double mode, set up items like before
|
||||
subItems.forEach {
|
||||
(it as? ReaderPage)?.shiftedPage = false
|
||||
}
|
||||
this.joinedItems = subItems.map { Pair<Any, Any?>(it, null) }.toMutableList()
|
||||
if (viewer is R2LPagerViewer) {
|
||||
joinedItems.reverse()
|
||||
}
|
||||
} else {
|
||||
val pagedItems = mutableListOf<MutableList<ReaderPage?>>()
|
||||
val otherItems = mutableListOf<Any>()
|
||||
pagedItems.add(mutableListOf())
|
||||
// Step 1: segment the pages and transition pages
|
||||
subItems.forEach {
|
||||
if (it is ReaderPage) {
|
||||
pagedItems.last().add(it)
|
||||
} else {
|
||||
otherItems.add(it)
|
||||
pagedItems.add(mutableListOf())
|
||||
}
|
||||
}
|
||||
var pagedIndex = 0
|
||||
val subJoinedItems = mutableListOf<Pair<Any, Any?>>()
|
||||
// Step 2: run through each set of pages
|
||||
pagedItems.forEach { items ->
|
||||
|
||||
items.forEach {
|
||||
it?.shiftedPage = false
|
||||
}
|
||||
// Step 3: If pages have been shifted,
|
||||
if (viewer.config.shiftDoublePage) {
|
||||
run loop@{
|
||||
var index = items.indexOf(pageToShift)
|
||||
if (pageToShift?.fullPage == true) {
|
||||
index = max(0, index - 1)
|
||||
}
|
||||
// Go from the current page and work your way back to the first page,
|
||||
// or the first page that's a full page.
|
||||
// This is done in case user tries to shift a page after a full page
|
||||
val fullPageBeforeIndex = max(
|
||||
0,
|
||||
(
|
||||
if (index > -1) (
|
||||
items.subList(0, index)
|
||||
.indexOfFirst { it?.fullPage == true }
|
||||
) else -1
|
||||
)
|
||||
)
|
||||
// Add a shifted page to the first place there isnt a full page
|
||||
(fullPageBeforeIndex until items.size).forEach {
|
||||
if (items[it]?.fullPage == false) {
|
||||
items[it]?.shiftedPage = true
|
||||
return@loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Add blanks for chunking
|
||||
var itemIndex = 0
|
||||
while (itemIndex < items.size) {
|
||||
items[itemIndex]?.isolatedPage = false
|
||||
if (items[itemIndex]?.fullPage == true || items[itemIndex]?.shiftedPage == true) {
|
||||
// Add a 'blank' page after each full page. It will be used when chunked to solo a page
|
||||
items.add(itemIndex + 1, null)
|
||||
if (items[itemIndex]?.fullPage == true && itemIndex > 0 &&
|
||||
items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0
|
||||
) {
|
||||
// If a page is a full page, check if the previous page needs to be isolated
|
||||
// we should check if it's an even or odd page, since even pages need shifting
|
||||
// For example if Page 1 is full, Page 0 needs to be isolated
|
||||
// No need to take account shifted pages, because null additions should
|
||||
// always have an odd index in the list
|
||||
items[itemIndex - 1]?.isolatedPage = true
|
||||
items.add(itemIndex, null)
|
||||
itemIndex++
|
||||
}
|
||||
itemIndex++
|
||||
}
|
||||
itemIndex++
|
||||
}
|
||||
|
||||
// Step 5: chunk em
|
||||
if (items.isNotEmpty()) {
|
||||
subJoinedItems.addAll(
|
||||
items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) }
|
||||
)
|
||||
}
|
||||
otherItems.getOrNull(pagedIndex)?.let {
|
||||
subJoinedItems.add(Pair(it, null))
|
||||
pagedIndex++
|
||||
}
|
||||
}
|
||||
if (viewer is R2LPagerViewer) {
|
||||
subJoinedItems.reverse()
|
||||
}
|
||||
|
||||
this.joinedItems = subJoinedItems
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
|
||||
// Step 6: Move back to our previous page or transition page
|
||||
// The listener is likely off around now, but either way when shifting or doubling,
|
||||
// we need to set the page back correctly
|
||||
// We will however shift to the first page of the new chapter if the last page we were are
|
||||
// on is not in the new chapter that has loaded
|
||||
val newPage =
|
||||
when {
|
||||
(oldCurrent?.first as? ReaderPage)?.chapter != currentChapter &&
|
||||
(oldCurrent?.first as? ChapterTransition)?.from != currentChapter -> subItems.find { (it as? ReaderPage)?.chapter == currentChapter }
|
||||
useSecondPage -> (oldCurrent?.second ?: oldCurrent?.first)
|
||||
else -> oldCurrent?.first ?: return
|
||||
}
|
||||
val index = joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
|
||||
viewer.pager.setCurrentItem(index, false)
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ class WebtoonViewer(val activity: ReaderActivity, val hasMargins: Boolean = fals
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
activity.onPageSelected(page)
|
||||
activity.onPageSelected(page, false)
|
||||
|
||||
val pages = page.chapter.pages ?: return
|
||||
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||
|
@ -5,6 +5,8 @@ import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PageLayout
|
||||
import eu.kanade.tachiyomi.util.lang.addBetaTag
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
@ -186,6 +188,21 @@ class SettingsReaderController : SettingsController() {
|
||||
titleRes = R.string.crop_borders
|
||||
defaultValue = false
|
||||
}
|
||||
intListPreference(activity) {
|
||||
key = Keys.pageLayout
|
||||
title = context.getString(R.string.page_layout).addBetaTag(context)
|
||||
dialogTitleRes = R.string.page_layout
|
||||
entriesRes = arrayOf(
|
||||
R.string.single_page,
|
||||
R.string.double_pages,
|
||||
R.string.automatic_orientation
|
||||
)
|
||||
entryRange = 0..2
|
||||
defaultValue = 2
|
||||
}
|
||||
infoPreference(R.string.automatic_can_still_switch).apply {
|
||||
preferences.pageLayout().asImmediateFlow { isVisible = it == PageLayout.AUTOMATIC }.launchIn(viewScope)
|
||||
}
|
||||
}
|
||||
preferenceCategory {
|
||||
titleRes = R.string.webtoon
|
||||
|
@ -1,11 +1,19 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import androidx.annotation.ColorInt
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
@ -90,3 +98,13 @@ fun String.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.addBetaTag(context: Context): Spanned {
|
||||
val betaText = context.getString(R.string.beta)
|
||||
val betaSpan = SpannableStringBuilder(this + betaText)
|
||||
betaSpan.setSpan(SuperscriptSpan(), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
betaSpan.setSpan(RelativeSizeSpan(0.75f), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
betaSpan.setSpan(StyleSpan(Typeface.BOLD), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
betaSpan.setSpan(ForegroundColorSpan(context.getResourceColor(R.attr.colorAccent)), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
return betaSpan
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.IO, block = block)
|
||||
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
|
@ -29,6 +29,13 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
private var pref: Preference<Int>? = null
|
||||
private var prefOffset = 0
|
||||
private var popup: PopupMenu? = null
|
||||
var title: CharSequence
|
||||
get() {
|
||||
return binding.titleView.text
|
||||
}
|
||||
set(value) {
|
||||
binding.titleView.text = value
|
||||
}
|
||||
|
||||
var onItemSelectedListener: ((Int) -> Unit)? = null
|
||||
set(value) {
|
||||
@ -49,7 +56,7 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.ReaderSpinnerView, 0, 0)
|
||||
|
||||
val str = a.getString(R.styleable.ReaderSpinnerView_title) ?: ""
|
||||
binding.titleView.text = str
|
||||
title = str
|
||||
|
||||
val entries = (a.getTextArray(R.styleable.ReaderSpinnerView_android_entries) ?: emptyArray()).map { it.toString() }
|
||||
this.entries = entries
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.widget.preference
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.Preference
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||
@ -23,6 +24,8 @@ open class MatPreference @JvmOverloads constructor(
|
||||
private var isShowing = false
|
||||
var customSummary: String? = null
|
||||
|
||||
@StringRes var dialogTitleRes: Int? = null
|
||||
|
||||
override fun onClick() {
|
||||
if (!isShowing) {
|
||||
dialog().apply {
|
||||
@ -38,7 +41,9 @@ open class MatPreference @JvmOverloads constructor(
|
||||
|
||||
open fun dialog(): MaterialDialog {
|
||||
return MaterialDialog(activity ?: context).apply {
|
||||
if (title != null) {
|
||||
if (dialogTitleRes != null) {
|
||||
title(res = dialogTitleRes)
|
||||
} else if (title != null) {
|
||||
title(text = title.toString())
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
|
9
app/src/main/res/drawable/ic_book_open_variant_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_book_open_variant_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<!-- drawable/book_open_variant.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/actionBarTintColor">
|
||||
<path android:fillColor="#000" android:pathData="M17.5 14.33C18.29 14.33 19.13 14.41 20 14.57V16.07C19.38 15.91 18.54 15.83 17.5 15.83C15.6 15.83 14.11 16.16 13 16.82V15.13C14.17 14.6 15.67 14.33 17.5 14.33M13 12.46C14.29 11.93 15.79 11.67 17.5 11.67C18.29 11.67 19.13 11.74 20 11.9V13.4C19.38 13.24 18.54 13.16 17.5 13.16C15.6 13.16 14.11 13.5 13 14.15M17.5 10.5C15.6 10.5 14.11 10.82 13 11.5V9.84C14.23 9.28 15.73 9 17.5 9C18.29 9 19.13 9.08 20 9.23V10.78C19.26 10.59 18.41 10.5 17.5 10.5M21 18.5V7C19.96 6.67 18.79 6.5 17.5 6.5C15.45 6.5 13.62 7 12 8V19.5C13.62 18.5 15.45 18 17.5 18C18.69 18 19.86 18.16 21 18.5M17.5 4.5C19.85 4.5 21.69 5 23 6V20.56C23 20.68 22.95 20.8 22.84 20.91C22.73 21 22.61 21.08 22.5 21.08C22.39 21.08 22.31 21.06 22.25 21.03C20.97 20.34 19.38 20 17.5 20C15.45 20 13.62 20.5 12 21.5C10.66 20.5 8.83 20 6.5 20C4.84 20 3.25 20.36 1.75 21.07C1.72 21.08 1.68 21.08 1.63 21.1C1.59 21.11 1.55 21.12 1.5 21.12C1.39 21.12 1.27 21.08 1.16 21C1.05 20.89 1 20.78 1 20.65V6C2.34 5 4.18 4.5 6.5 4.5C8.83 4.5 10.66 5 12 6C13.34 5 15.17 4.5 17.5 4.5Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_outline_photo_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_outline_photo_24dp.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/actionBarTintColor">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_outline_save_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_outline_save_24dp.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/actionBarTintColor">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_outline_share_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_outline_share_24dp.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/actionBarTintColor">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_page_next_outline_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_page_next_outline_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<!-- drawable/page_next_outline.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:tint="?attr/actionBarTintColor"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M22,3H5A2,2 0 0,0 3,5V9H5V5H22V19H5V15H3V19A2,2 0 0,0 5,21H22A2,2 0 0,0 24,19V5A2,2 0 0,0 22,3M7,15V13H0V11H7V9L11,12L7,15M20,13H13V11H20V13M20,9H13V7H20V9M17,17H13V15H17V17Z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<!-- drawable/page_previous_outline.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:tint="?attr/actionBarTintColor"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M2,3H19A2,2 0 0,1 21,5V9H19V5H2V19H19V15H21V19A2,2 0 0,1 19,21H2A2,2 0 0,1 0,19V5A2,2 0 0,1 2,3M17,15V13H24V11H17V9L13,12L17,15M4,13H11V11H4V13M4,9H11V7H4V9M4,17H8V15H4V17Z" />
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_single_page_24dp.xml
Normal file
11
app/src/main/res/drawable/ic_single_page_24dp.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/actionBarTintColor"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
|
||||
</vector>
|
@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:end="96dp" android:start="64dp" android:drawable="@drawable/square_ripple"/>
|
||||
<item android:id="@+id/layer_full_width" android:end="0dp" android:start="64dp" android:drawable="@drawable/rect_ripple"/>
|
||||
<item android:id="@+id/layer_one_item" android:end="48dp" android:start="64dp" android:drawable="@drawable/rect_ripple" />
|
||||
</layer-list>
|
9
app/src/main/res/drawable/rect_ripple.xml
Normal file
9
app/src/main/res/drawable/rect_ripple.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/fullRippleColor">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/fullRippleColor" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
@ -59,10 +59,26 @@
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/chapters_button"
|
||||
app:layout_constraintEnd_toStartOf="@id/display_options"
|
||||
app:layout_constraintEnd_toStartOf="@id/double_page"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_open_in_webview_24dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/double_page"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/next_title"
|
||||
android:padding="@dimen/material_layout_keylines_screen_edge_margin"
|
||||
app:tint="?actionBarTintColor"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
android:tooltipText="@string/double_pages"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/webview_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/display_options"
|
||||
app:srcCompat="@drawable/ic_book_open_variant_24dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/display_options"
|
||||
android:layout_width="wrap_content"
|
||||
@ -74,7 +90,7 @@
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
android:tooltipText="@string/display_options"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/webview_button"
|
||||
app:layout_constraintStart_toEndOf="@id/double_page"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_tune_24dp" />
|
||||
|
@ -46,12 +46,12 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/left_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/textColorPrimary"
|
||||
android:textSize="15sp"
|
||||
tools:text="1" />
|
||||
tools:text="12-14" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderSeekBar
|
||||
android:id="@+id/page_seekbar"
|
||||
@ -63,7 +63,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/right_page_text"
|
||||
android:layout_width="32dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/textColorPrimary"
|
||||
|
@ -40,6 +40,14 @@
|
||||
app:title="@string/scale_type"
|
||||
android:entries="@array/image_scale_type" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
|
||||
android:id="@+id/page_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
app:title="@string/page_layout"
|
||||
android:entries="@array/page_layouts" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
|
||||
android:id="@+id/extend_past_cutout"
|
||||
android:layout_width="match_parent"
|
||||
@ -56,7 +64,6 @@
|
||||
app:title="@string/zoom_start_position"
|
||||
android:entries="@array/zoom_start" />
|
||||
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/crop_borders"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -2,4 +2,9 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_shift_double_page"
|
||||
android:icon="@drawable/ic_page_next_outline_24dp"
|
||||
android:title="@string/shift_one_page_over"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
@ -70,6 +70,12 @@
|
||||
<item>@string/right_and_left_nav</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="page_layouts">
|
||||
<item>@string/single_page</item>
|
||||
<item>@string/double_pages</item>
|
||||
<item>@string/automatic</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="invert_tapping_mode">
|
||||
<item>@string/none</item>
|
||||
<item>@string/horizontally</item>
|
||||
|
@ -165,6 +165,7 @@
|
||||
<string name="hide_category_hopper">Hide category hopper</string>
|
||||
<string name="hide_hopper_on_scroll">Hide category hopper on scroll</string>
|
||||
<string name="more_library_settings">More library settings</string>
|
||||
<string name="shift_one_page_over">Shift one page over</string>
|
||||
|
||||
<!-- Library update service notifications -->
|
||||
<string name="new_chapters_found">New chapters found</string>
|
||||
@ -279,9 +280,6 @@
|
||||
<!-- Reader -->
|
||||
<string name="custom_filter">Custom filter</string>
|
||||
<string name="set_as_cover">Set as cover</string>
|
||||
<string name="set_page_as_cover">Set page as cover</string>
|
||||
<string name="share_page">Share page</string>
|
||||
<string name="save_page">Save page</string>
|
||||
<string name="_details">%1$s details</string>
|
||||
<string name="reader_settings">Reader settings</string>
|
||||
<string name="set_as_default_for_all">Set as default for all</string>
|
||||
@ -306,6 +304,14 @@
|
||||
<string name="next_chapter">Next chapter</string>
|
||||
<string name="previous_chapter">Previous chapter</string>
|
||||
|
||||
<string name="set_first_page_as_cover">Set first page as cover</string>
|
||||
<string name="share_first_page">Share first page</string>
|
||||
<string name="save_first_page">Save first page</string>
|
||||
|
||||
<string name="set_second_page_as_cover">Set second page as cover</string>
|
||||
<string name="share_second_page">Share second page</string>
|
||||
<string name="save_second_page">Save second page</string>
|
||||
|
||||
<!-- Reader settings -->
|
||||
<string name="fullscreen">Fullscreen</string>
|
||||
<string name="animate_page_transitions">Animate page transitions</string>
|
||||
@ -352,6 +358,10 @@
|
||||
<string name="original_size">Original size</string>
|
||||
<string name="smart_fit">Smart fit</string>
|
||||
<string name="zoom_start_position">Zoom start position</string>
|
||||
<string name="double_pages">Double pages</string>
|
||||
<string name="single_page">Single page</string>
|
||||
<string name="switch_to_double">Switch to double pages</string>
|
||||
<string name="switch_to_single">Switch to single page</string>
|
||||
<string name="force_portrait">Force portrait</string>
|
||||
<string name="force_landscape">Force landscape</string>
|
||||
<string name="red_initial">R</string>
|
||||
@ -382,6 +392,9 @@
|
||||
<string name="pad_cutout_areas">Pad cutout areas</string>
|
||||
<string name="start_past_cutout">Start past cutout</string>
|
||||
<string name="ignore_cutout_areas">Ignore cutout areas</string>
|
||||
<string name="page_layout">Page layout</string>
|
||||
<string name="automatic_can_still_switch">While using automatic page layout, you can still switch between layouts while reading without overriding this setting</string>
|
||||
<string name="automatic_orientation">Automatic (based on orientation)</string>
|
||||
|
||||
<!-- Manga details -->
|
||||
<string name="about_this_">About this %1$s</string>
|
||||
@ -783,6 +796,7 @@
|
||||
<string name="auto">Auto</string>
|
||||
<string name="automatic">Automatic</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="beta">BETA</string>
|
||||
<string name="bottom">Bottom</string>
|
||||
<string name="bug_report">Report a Bug</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
Loading…
Reference in New Issue
Block a user