Преглед на файлове

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 <[email protected]>
vance преди 4 години
родител
ревизия
b5017eebbf

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt

@@ -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"

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -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)

+ 1 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt

@@ -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())

+ 10 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt

@@ -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
+    }
+}

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt

@@ -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
 }

+ 4 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt

@@ -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

+ 36 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt

@@ -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.
      */

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt

@@ -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)
+    }
 }

+ 32 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt

@@ -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()
+    }
 }

+ 8 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt

@@ -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

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt

@@ -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

+ 73 - 0
app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt

@@ -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
+    }
 }

+ 9 - 1
app/src/main/res/layout/reader_settings_sheet.xml

@@ -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

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -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>