mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 19:11:50 +01:00
Added dual page split setting (#4252)
* Add DualPageSplit option * remove extra line * Split double-page into two pages * Remove !isAnimated check and add (ALPHA) to the label * Fix missing insert pages * Pager cleanup * Add dual split to Webtoon and fix Vertical * Fix L2R/R2L * Add comments and refactor code in ImageUtil * Use a simpler split solution in webtoon mode Co-authored-by: weng <> Co-authored-by: Andreas E <andreas.everos@gmail.com>
This commit is contained in:
parent
aa67229daf
commit
b5017eebbf
@ -23,6 +23,8 @@ object PreferenceKeys {
|
||||
|
||||
const val showPageNumber = "pref_show_page_number_key"
|
||||
|
||||
const val dualPageSplit = "pref_dual_page_split"
|
||||
|
||||
const val showReadingMode = "pref_show_reading_mode"
|
||||
|
||||
const val trueColor = "pref_true_color_key"
|
||||
|
@ -89,6 +89,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||
|
||||
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
|
||||
|
||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||
|
||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
||||
|
@ -65,6 +65,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BaseBottomShee
|
||||
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
|
||||
binding.showPageNumber.bindToPreference(preferences.showPageNumber())
|
||||
binding.fullscreen.bindToPreference(preferences.fullscreen())
|
||||
binding.dualPageSplit.bindToPreference(preferences.dualPageSplit())
|
||||
binding.keepscreen.bindToPreference(preferences.keepScreenOn())
|
||||
binding.longTap.bindToPreference(preferences.readWithLongTap())
|
||||
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())
|
||||
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.model
|
||||
|
||||
class InsertPage(val parent: ReaderPage) : ReaderPage(parent.index, parent.url, parent.imageUrl) {
|
||||
|
||||
override var chapter: ReaderChapter = parent.chapter
|
||||
|
||||
init {
|
||||
stream = parent.stream
|
||||
}
|
||||
}
|
@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.model
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import java.io.InputStream
|
||||
|
||||
class ReaderPage(
|
||||
open class ReaderPage(
|
||||
index: Int,
|
||||
url: String = "",
|
||||
imageUrl: String? = null,
|
||||
var stream: (() -> InputStream)? = null
|
||||
) : Page(index, url, imageUrl, null) {
|
||||
|
||||
lateinit var chapter: ReaderChapter
|
||||
open lateinit var chapter: ReaderChapter
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ abstract class ViewerConfig(preferences: PreferencesHelper, private val scope: C
|
||||
var volumeKeysInverted = false
|
||||
var trueColor = false
|
||||
var alwaysShowChapterTransition = true
|
||||
var dualPageSplit = false
|
||||
var navigationMode = 0
|
||||
protected set
|
||||
|
||||
@ -54,6 +55,9 @@ abstract class ViewerConfig(preferences: PreferencesHelper, private val scope: C
|
||||
|
||||
preferences.alwaysShowChapterTransition()
|
||||
.register({ alwaysShowChapterTransition = it })
|
||||
|
||||
preferences.dualPageSplit()
|
||||
.register({ dualPageSplit = it }, { imagePropertyChangedListener?.invoke() })
|
||||
}
|
||||
|
||||
protected abstract fun defaultNavigation(): ViewerNavigation
|
||||
|
@ -28,6 +28,7 @@ import com.github.chrisbanes.photoview.PhotoView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
|
||||
@ -241,6 +242,9 @@ class PagerPageHolder(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { isAnimated ->
|
||||
if (viewer.config.dualPageSplit) {
|
||||
openStream = processDualPageSplit(openStream!!)
|
||||
}
|
||||
if (!isAnimated) {
|
||||
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
|
||||
} else {
|
||||
@ -253,6 +257,38 @@ class PagerPageHolder(
|
||||
.subscribe({}, {})
|
||||
}
|
||||
|
||||
private fun processDualPageSplit(openStream: InputStream): InputStream {
|
||||
var inputStream = openStream
|
||||
val (isDoublePage, stream) = when (page) {
|
||||
is InsertPage -> Pair(true, inputStream)
|
||||
else -> ImageUtil.isDoublePage(inputStream)
|
||||
}
|
||||
inputStream = stream
|
||||
if (isDoublePage) {
|
||||
val side = when {
|
||||
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
|
||||
viewer is R2LPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
|
||||
viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT
|
||||
viewer is R2LPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
|
||||
viewer is VerticalPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
|
||||
viewer is VerticalPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
|
||||
else -> error("We should choose a side!")
|
||||
}
|
||||
|
||||
if (page !is InsertPage) {
|
||||
onPageSplit()
|
||||
}
|
||||
|
||||
inputStream = ImageUtil.splitInHalf(inputStream, side)
|
||||
}
|
||||
return inputStream
|
||||
}
|
||||
|
||||
private fun onPageSplit() {
|
||||
val newPage = InsertPage(page)
|
||||
viewer.onPageSplit(page, newPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page has an error.
|
||||
*/
|
||||
|
@ -12,6 +12,7 @@ import androidx.viewpager.widget.ViewPager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
@ -371,4 +372,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
|
||||
adapter.onPageSplit(currentPage, newPage, this::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
@ -18,7 +19,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
/**
|
||||
* List of currently set items.
|
||||
*/
|
||||
var items: List<Any> = emptyList()
|
||||
var items: MutableList<Any> = mutableListOf()
|
||||
private set
|
||||
|
||||
var nextTransition: ChapterTransition.Next? = null
|
||||
@ -80,6 +81,9 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Resets double-page splits, else insert pages get misplaced
|
||||
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
|
||||
|
||||
if (viewer is R2LPagerViewer) {
|
||||
newItems.reverse()
|
||||
}
|
||||
@ -120,4 +124,31 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
return POSITION_NONE
|
||||
}
|
||||
|
||||
fun onPageSplit(current: Any?, newPage: InsertPage, clazz: Class<out PagerViewer>) {
|
||||
if (current !is ReaderPage) return
|
||||
|
||||
val currentIndex = items.indexOf(current)
|
||||
|
||||
val placeAtIndex = when {
|
||||
clazz.isAssignableFrom(L2RPagerViewer::class.java) -> currentIndex + 1
|
||||
clazz.isAssignableFrom(VerticalPagerViewer::class.java) -> currentIndex + 1
|
||||
clazz.isAssignableFrom(R2LPagerViewer::class.java) -> currentIndex
|
||||
else -> currentIndex
|
||||
}
|
||||
|
||||
// It will enter a endless cycle of insert pages
|
||||
if (clazz.isAssignableFrom(R2LPagerViewer::class.java) && items[placeAtIndex - 1] is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Same here it will enter a endless cycle of insert pages
|
||||
if (items[placeAtIndex] is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
items.add(placeAtIndex, newPage)
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
@ -287,6 +287,14 @@ class WebtoonPageHolder(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { isAnimated ->
|
||||
if (viewer.config.dualPageSplit) {
|
||||
val (isDoublePage, stream) = ImageUtil.isDoublePage(openStream!!)
|
||||
openStream = if (!isDoublePage) {
|
||||
stream
|
||||
} else {
|
||||
ImageUtil.splitAndMerge(stream)
|
||||
}
|
||||
}
|
||||
if (!isAnimated) {
|
||||
val subsamplingView = initSubsamplingImageView()
|
||||
subsamplingView.isVisible = true
|
||||
|
@ -50,6 +50,11 @@ class SettingsReaderController : SettingsController() {
|
||||
summaryRes = R.string.pref_show_reading_mode_summary
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.dualPageSplit
|
||||
titleRes = R.string.pref_dual_page_split
|
||||
defaultValue = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
switchPreference {
|
||||
key = Keys.trueColor
|
||||
|
@ -1,5 +1,11 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
|
||||
@ -68,4 +74,71 @@ object ImageUtil {
|
||||
GIF("image/gif", "gif"),
|
||||
WEBP("image/webp", "webp")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the image is a double image (width > height), return the result and original stream
|
||||
*/
|
||||
fun isDoublePage(imageStream: InputStream): Pair<Boolean, InputStream> {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
||||
|
||||
return Pair(options.outWidth > options.outHeight, ByteArrayInputStream(imageBytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the 'side' part from imageStream and return it as InputStream.
|
||||
*/
|
||||
fun splitInHalf(imageStream: InputStream, side: Side): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val singlePage = Rect(0, 0, width / 2, height)
|
||||
|
||||
val half = Bitmap.createBitmap(width / 2, height, Bitmap.Config.ARGB_8888)
|
||||
val part = when (side) {
|
||||
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.LEFT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
val canvas = Canvas(half)
|
||||
canvas.drawBitmap(imageBitmap, part, singlePage, null)
|
||||
val output = ByteArrayOutputStream()
|
||||
half.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the image into left and right parts, then merge them into a new image.
|
||||
*/
|
||||
fun splitAndMerge(imageStream: InputStream): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val result = Bitmap.createBitmap(width / 2, height * 2, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
// right -> upper
|
||||
val rightPart = Rect(width - width / 2, 0, width, height)
|
||||
val upperPart = Rect(0, 0, width / 2, height)
|
||||
canvas.drawBitmap(imageBitmap, rightPart, upperPart, null)
|
||||
// left -> bottom
|
||||
val leftPart = Rect(0, 0, width / 2, height)
|
||||
val bottomPart = Rect(0, height, width / 2, height * 2)
|
||||
canvas.drawBitmap(imageBitmap, leftPart, bottomPart, null)
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
enum class Side {
|
||||
RIGHT, LEFT
|
||||
}
|
||||
}
|
||||
|
@ -151,6 +151,14 @@
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/show_page_number" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/dual_page_split"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_dual_page_split"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/fullscreen" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/cutout_short"
|
||||
android:layout_width="match_parent"
|
||||
@ -158,7 +166,7 @@
|
||||
android:text="@string/pref_cutout_short"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/fullscreen"
|
||||
app:layout_constraintTop_toBottomOf="@id/dual_page_split"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
|
@ -250,6 +250,7 @@
|
||||
|
||||
<!-- Reader section -->
|
||||
<string name="pref_fullscreen">Fullscreen</string>
|
||||
<string name="pref_dual_page_split">Dual page split (ALPHA)</string>
|
||||
<string name="pref_cutout_short">Show content in cutout area</string>
|
||||
<string name="pref_lock_orientation">Lock orientation</string>
|
||||
<string name="pref_page_transitions">Animate page transitions</string>
|
||||
|
Loading…
Reference in New Issue
Block a user