Преглед изворни кода

同步上游更改: @lhjgege
新漫画增加墨水屏模式 (#5181)
* 新漫画增加墨水屏模式
* 增加水平滚动动画开关

lhjgege пре 1 година
родитељ
комит
06dd2ac2c1

+ 5 - 0
app/src/main/java/io/legato/kazusa/constant/PreferKey.kt

@@ -104,6 +104,11 @@ object PreferKey {
     const val enableMangaHorizontalScroll = "enableMangaHorizontalScroll"
     const val enableMangaHorizontalScroll = "enableMangaHorizontalScroll"
     const val hideMangaTitle = "hideMangaTitle"
     const val hideMangaTitle = "hideMangaTitle"
     const val mangaColorFilter = "mangaColorFilter"
     const val mangaColorFilter = "mangaColorFilter"
+    const val enableMangaEInk = "enableMangaEInk"
+    const val mangaEInkThreshold = "mangaEInkThreshold"
+    const val disableHorizontalAnimator = "disableHorizontalAnimator"
+    const val enableMangaGray = "enableMangaGray"
+
     const val autoRefresh = "auto_refresh"
     const val autoRefresh = "auto_refresh"
     const val defaultToRead = "defaultToRead"
     const val defaultToRead = "defaultToRead"
     const val exportCharset = "exportCharset"
     const val exportCharset = "exportCharset"

+ 26 - 1
app/src/main/java/io/legato/kazusa/help/config/AppConfig.kt

@@ -496,7 +496,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
 
 
     val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf")
     val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf")
 
 
-    val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "default_version")
+    val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "official_version")
 
 
     val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false)
     val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false)
 
 
@@ -676,5 +676,30 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
         set(value) {
         set(value) {
             appCtx.putPrefBoolean(PreferKey.hideMangaTitle, value)
             appCtx.putPrefBoolean(PreferKey.hideMangaTitle, value)
         }
         }
+
+    //开启墨水屏模式
+    var enableMangaEInk
+        get() = appCtx.getPrefBoolean(PreferKey.enableMangaEInk, false)
+        set(value) {
+            appCtx.putPrefBoolean(PreferKey.enableMangaEInk, value)
+        }
+
+    var mangaEInkThreshold
+        get() = appCtx.getPrefInt(PreferKey.mangaEInkThreshold, 150)
+        set(value) {
+            appCtx.putPrefInt(PreferKey.mangaEInkThreshold, value)
+        }
+
+    var disableHorizontalAnimator
+        get() = appCtx.getPrefBoolean(PreferKey.disableHorizontalAnimator, false)
+        set(value) {
+            appCtx.putPrefBoolean(PreferKey.disableHorizontalAnimator, value)
+        }
+
+    var enableMangaGray
+        get() = appCtx.getPrefBoolean(PreferKey.enableMangaGray, false)
+        set(value) {
+            appCtx.putPrefBoolean(PreferKey.enableMangaGray, value)
+        }
 }
 }
 
 

+ 15 - 6
app/src/main/java/io/legato/kazusa/model/BookCover.kt

@@ -2,12 +2,14 @@ package io.legato.kazusa.model
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context
-import android.graphics.drawable.BitmapDrawable
+import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.Drawable
 import androidx.annotation.Keep
 import androidx.annotation.Keep
+import androidx.core.graphics.drawable.toDrawable
 import com.bumptech.glide.Glide
 import com.bumptech.glide.Glide
 import com.bumptech.glide.RequestBuilder
 import com.bumptech.glide.RequestBuilder
 import com.bumptech.glide.load.DataSource
 import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.Transformation
 import com.bumptech.glide.load.engine.DiskCacheStrategy
 import com.bumptech.glide.load.engine.DiskCacheStrategy
 import com.bumptech.glide.load.engine.GlideException
 import com.bumptech.glide.load.engine.GlideException
 import com.bumptech.glide.load.resource.bitmap.CenterCrop
 import com.bumptech.glide.load.resource.bitmap.CenterCrop
@@ -74,7 +76,7 @@ object BookCover {
             return
             return
         }
         }
         defaultDrawable = kotlin.runCatching {
         defaultDrawable = kotlin.runCatching {
-            BitmapDrawable(appCtx.resources, BitmapUtils.decodeBitmap(path, 600, 900))
+            BitmapUtils.decodeBitmap(path, 600, 900).toDrawable(appCtx.resources)
         }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
         }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
     }
     }
 
 
@@ -87,7 +89,7 @@ object BookCover {
         path: String?,
         path: String?,
         loadOnlyWifi: Boolean = false,
         loadOnlyWifi: Boolean = false,
         sourceOrigin: String? = null,
         sourceOrigin: String? = null,
-        onLoadFinish: (() -> Unit)? = null
+        onLoadFinish: (() -> Unit)? = null,
     ): RequestBuilder<Drawable> {
     ): RequestBuilder<Drawable> {
         if (AppConfig.useDefaultCover) {
         if (AppConfig.useDefaultCover) {
             return ImageLoader.load(context, defaultDrawable)
             return ImageLoader.load(context, defaultDrawable)
@@ -105,7 +107,7 @@ object BookCover {
                     e: GlideException?,
                     e: GlideException?,
                     model: Any?,
                     model: Any?,
                     target: Target<Drawable?>,
                     target: Target<Drawable?>,
-                    isFirstResource: Boolean
+                    isFirstResource: Boolean,
                 ): Boolean {
                 ): Boolean {
                     onLoadFinish.invoke()
                     onLoadFinish.invoke()
                     return false
                     return false
@@ -116,7 +118,7 @@ object BookCover {
                     model: Any,
                     model: Any,
                     target: Target<Drawable?>?,
                     target: Target<Drawable?>?,
                     dataSource: DataSource,
                     dataSource: DataSource,
-                    isFirstResource: Boolean
+                    isFirstResource: Boolean,
                 ): Boolean {
                 ): Boolean {
                     onLoadFinish.invoke()
                     onLoadFinish.invoke()
                     return false
                     return false
@@ -133,11 +135,13 @@ object BookCover {
     /**
     /**
      * 加载漫画图片
      * 加载漫画图片
      */
      */
+    @SuppressLint("CheckResult")
     fun loadManga(
     fun loadManga(
         context: Context,
         context: Context,
         path: String?,
         path: String?,
         loadOnlyWifi: Boolean = false,
         loadOnlyWifi: Boolean = false,
         sourceOrigin: String? = null,
         sourceOrigin: String? = null,
+        transformation: Transformation<Bitmap>? = null,
     ): RequestBuilder<Drawable> {
     ): RequestBuilder<Drawable> {
         var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
         var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
             .set(OkHttpModelLoader.mangaOption, true)
             .set(OkHttpModelLoader.mangaOption, true)
@@ -148,7 +152,12 @@ object BookCover {
             .apply(options)
             .apply(options)
             .override(context.resources.displayMetrics.widthPixels, SIZE_ORIGINAL)
             .override(context.resources.displayMetrics.widthPixels, SIZE_ORIGINAL)
             .diskCacheStrategy(DiskCacheStrategy.ALL)
             .diskCacheStrategy(DiskCacheStrategy.ALL)
-            .skipMemoryCache(true)
+            .skipMemoryCache(true).let {
+                if (transformation != null) {
+                    it.transform(transformation)
+                }
+                it
+            }
     }
     }
 
 
     fun preloadManga(
     fun preloadManga(

+ 12 - 0
app/src/main/java/io/legato/kazusa/ui/about/UpdateDialog.kt

@@ -6,8 +6,11 @@ import android.view.View
 import io.legato.kazusa.BuildConfig
 import io.legato.kazusa.BuildConfig
 import io.legato.kazusa.R
 import io.legato.kazusa.R
 import io.legato.kazusa.base.BaseBottomSheetDialogFragment
 import io.legato.kazusa.base.BaseBottomSheetDialogFragment
+import io.legato.kazusa.constant.AppConst
 import io.legato.kazusa.databinding.DialogUpdateBinding
 import io.legato.kazusa.databinding.DialogUpdateBinding
+import io.legato.kazusa.help.config.AppConfig
 import io.legato.kazusa.help.update.AppUpdate
 import io.legato.kazusa.help.update.AppUpdate
+import io.legato.kazusa.help.update.AppVariant
 import io.legato.kazusa.model.Download
 import io.legato.kazusa.model.Download
 import io.legato.kazusa.utils.toastOnUi
 import io.legato.kazusa.utils.toastOnUi
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
@@ -27,6 +30,14 @@ class UpdateDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_update) {
         }
         }
     }
     }
 
 
+    private val checkVariant: AppVariant
+        get() = when (AppConfig.updateToVariant) {
+            "official_version" -> AppVariant.OFFICIAL
+            "beta_release_version" -> AppVariant.BETA_RELEASE
+            "beta_releaseA_version" -> AppVariant.BETA_RELEASEA
+            else -> AppConst.appInfo.appVariant
+        }
+
     val binding by viewBinding(DialogUpdateBinding::bind)
     val binding by viewBinding(DialogUpdateBinding::bind)
 
 
     override fun onStart() {
     override fun onStart() {
@@ -41,6 +52,7 @@ class UpdateDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_update) {
 
 
         binding.tvAbi.text = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
         binding.tvAbi.text = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
         binding.tvUrl.text = arguments?.getString("url")
         binding.tvUrl.text = arguments?.getString("url")
+        binding.tvVariable.text = checkVariant.toString()
 
 
         val updateBody = arguments?.getString("updateBody")
         val updateBody = arguments?.getString("updateBody")
         if (updateBody == null) {
         if (updateBody == null) {

+ 54 - 3
app/src/main/java/io/legato/kazusa/ui/book/manga/ReadMangaActivity.kt

@@ -5,7 +5,6 @@ import android.content.Intent
 import android.os.Build
 import android.os.Build
 import android.os.Bundle
 import android.os.Bundle
 import android.os.Looper
 import android.os.Looper
-import android.os.PersistableBundle
 import android.view.KeyEvent
 import android.view.KeyEvent
 import android.view.Menu
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MenuItem
@@ -44,6 +43,7 @@ import io.legato.kazusa.ui.book.changesource.ChangeBookSourceDialog
 import io.legato.kazusa.ui.book.info.BookInfoActivity
 import io.legato.kazusa.ui.book.info.BookInfoActivity
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterDialog
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterDialog
+import io.legato.kazusa.ui.book.manga.config.MangaEpaperDialog
 import io.legato.kazusa.ui.book.manga.config.MangaFooterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaFooterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaFooterSettingDialog
 import io.legato.kazusa.ui.book.manga.config.MangaFooterSettingDialog
 import io.legato.kazusa.ui.book.manga.entities.BaseMangaPage
 import io.legato.kazusa.ui.book.manga.entities.BaseMangaPage
@@ -63,7 +63,6 @@ import io.legato.kazusa.utils.canScroll
 import io.legato.kazusa.utils.fastBinarySearch
 import io.legato.kazusa.utils.fastBinarySearch
 import io.legato.kazusa.utils.findCenterViewPosition
 import io.legato.kazusa.utils.findCenterViewPosition
 import io.legato.kazusa.utils.fromJsonObject
 import io.legato.kazusa.utils.fromJsonObject
-import io.legato.kazusa.utils.getCompatColor
 import io.legato.kazusa.utils.gone
 import io.legato.kazusa.utils.gone
 import io.legato.kazusa.utils.observeEvent
 import io.legato.kazusa.utils.observeEvent
 import io.legato.kazusa.utils.showDialogFragment
 import io.legato.kazusa.utils.showDialogFragment
@@ -79,7 +78,7 @@ import kotlin.math.ceil
 
 
 class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewModel>(),
 class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewModel>(),
     ReadManga.Callback, ChangeBookSourceDialog.CallBack, MangaMenu.CallBack,
     ReadManga.Callback, ChangeBookSourceDialog.CallBack, MangaMenu.CallBack,
-    MangaColorFilterDialog.Callback, ScrollTimer.ScrollCallback {
+    MangaColorFilterDialog.Callback, ScrollTimer.ScrollCallback, MangaEpaperDialog.Callback {
 
 
     private val mLayoutManager by lazy {
     private val mLayoutManager by lazy {
         MangaLayoutManager(this)
         MangaLayoutManager(this)
@@ -191,6 +190,8 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
                 ?: MangaColorFilterConfig()
                 ?: MangaColorFilterConfig()
         mAdapter.run {
         mAdapter.run {
             setMangaImageColorFilter(mangaColorFilter)
             setMangaImageColorFilter(mangaColorFilter)
+            enableMangaEInk(AppConfig.enableMangaEInk, AppConfig.mangaEInkThreshold)
+            enableGray(AppConfig.enableMangaGray)
         }
         }
         setHorizontalScroll(AppConfig.enableMangaHorizontalScroll)
         setHorizontalScroll(AppConfig.enableMangaHorizontalScroll)
         binding.recyclerView.run {
         binding.recyclerView.run {
@@ -541,6 +542,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
             R.id.menu_enable_horizontal_scroll -> {
             R.id.menu_enable_horizontal_scroll -> {
                 item.isChecked = !item.isChecked
                 item.isChecked = !item.isChecked
                 AppConfig.enableMangaHorizontalScroll = item.isChecked
                 AppConfig.enableMangaHorizontalScroll = item.isChecked
+                mMenu?.findItem(R.id.menu_disable_horizontal_animation)?.isVisible = item.isChecked
                 setHorizontalScroll(item.isChecked)
                 setHorizontalScroll(item.isChecked)
                 mAdapter.notifyDataSetChanged()
                 mAdapter.notifyDataSetChanged()
             }
             }
@@ -570,6 +572,38 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
                 AppConfig.hideMangaTitle = item.isChecked
                 AppConfig.hideMangaTitle = item.isChecked
                 ReadManga.loadContent()
                 ReadManga.loadContent()
             }
             }
+
+            R.id.menu_epaper_manga -> {
+                item.isChecked = !item.isChecked
+                AppConfig.enableMangaEInk = item.isChecked
+                mMenu?.findItem(R.id.menu_gray_manga)?.isChecked = false
+                AppConfig.enableMangaGray = false
+                mMenu?.findItem(R.id.menu_epaper_manga_setting)?.isVisible = item.isChecked
+                mAdapter.enableMangaEInk(item.isChecked, AppConfig.mangaEInkThreshold)
+            }
+
+            R.id.menu_epaper_manga_setting -> {
+                showDialogFragment(MangaEpaperDialog())
+            }
+
+            R.id.menu_disable_horizontal_animation -> {
+                item.isChecked = !item.isChecked
+                AppConfig.disableHorizontalAnimator = item.isChecked
+                if (item.isChecked) {
+                    mPagerSnapHelper.attachToRecyclerView(null)
+                } else {
+                    mPagerSnapHelper.attachToRecyclerView(binding.recyclerView)
+                }
+            }
+
+            R.id.menu_gray_manga -> {
+                item.isChecked = !item.isChecked
+                AppConfig.enableMangaGray = item.isChecked
+                mMenu?.findItem(R.id.menu_epaper_manga)?.isChecked = false
+                AppConfig.enableMangaEInk = false
+                mMenu?.findItem(R.id.menu_epaper_manga_setting)?.isVisible = false
+                mAdapter.enableGray(item.isChecked)
+            }
         }
         }
         return super.onCompatOptionsItemSelected(item)
         return super.onCompatOptionsItemSelected(item)
     }
     }
@@ -626,6 +660,11 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         if (enable) {
         if (enable) {
             if (!enableAutoScroll) {
             if (!enableAutoScroll) {
                 mPagerSnapHelper.attachToRecyclerView(binding.recyclerView)
                 mPagerSnapHelper.attachToRecyclerView(binding.recyclerView)
+                if (AppConfig.disableHorizontalAnimator) {
+                    mPagerSnapHelper.attachToRecyclerView(null)
+                } else {
+                    mPagerSnapHelper.attachToRecyclerView(binding.recyclerView)
+                }
             }
             }
             mLayoutManager.orientation = LinearLayoutManager.HORIZONTAL
             mLayoutManager.orientation = LinearLayoutManager.HORIZONTAL
         } else {
         } else {
@@ -646,6 +685,14 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         menu.findItem(R.id.menu_enable_horizontal_scroll).isChecked =
         menu.findItem(R.id.menu_enable_horizontal_scroll).isChecked =
             AppConfig.enableMangaHorizontalScroll
             AppConfig.enableMangaHorizontalScroll
         menu.findItem(R.id.menu_hide_manga_title).isChecked = AppConfig.hideMangaTitle
         menu.findItem(R.id.menu_hide_manga_title).isChecked = AppConfig.hideMangaTitle
+        menu.findItem(R.id.menu_epaper_manga).isChecked = AppConfig.enableMangaEInk
+        menu.findItem(R.id.menu_epaper_manga_setting).isVisible = AppConfig.enableMangaEInk
+        menu.findItem(R.id.menu_disable_horizontal_animation).run {
+            isVisible =
+                AppConfig.enableMangaHorizontalScroll
+            isChecked = AppConfig.disableHorizontalAnimator
+        }
+        menu.findItem(R.id.menu_gray_manga).isChecked = AppConfig.enableMangaGray
     }
     }
 
 
     private fun setDisableMangaScale(disable: Boolean) {
     private fun setDisableMangaScale(disable: Boolean) {
@@ -784,4 +831,8 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         }
         }
         return super.onKeyDown(keyCode, event)
         return super.onKeyDown(keyCode, event)
     }
     }
+
+    override fun updateEepaper(value: Int) {
+        mAdapter.updateThreshold(value)
+    }
 }
 }

+ 49 - 0
app/src/main/java/io/legato/kazusa/ui/book/manga/config/MangaEpaperDialog.kt

@@ -0,0 +1,49 @@
+package io.legato.kazusa.ui.book.manga.config
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import io.legato.kazusa.R
+import io.legato.kazusa.base.BaseBottomSheetDialogFragment
+import io.legato.kazusa.databinding.DialogMangaEpaperBinding
+import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
+
+
+class MangaEpaperDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_epaper) {
+    private val binding by viewBinding(DialogMangaEpaperBinding::bind)
+    private val callback get() = activity as? Callback
+    private var mMangaEInkThreshold = 150
+
+//    override fun onStart() {
+//        super.onStart()
+//        dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+//        setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+//    }
+
+    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
+        initData()
+        initView()
+    }
+
+    private fun initData() {
+        binding.dsbEpaper.progress = AppConfig.mangaEInkThreshold
+    }
+
+    private fun initView() {
+        binding.dsbEpaper.onChanged = {
+            mMangaEInkThreshold = it
+            callback?.updateEepaper(it)
+        }
+    }
+
+    override fun onDismiss(dialog: DialogInterface) {
+        super.onDismiss(dialog)
+        AppConfig.mangaEInkThreshold = mMangaEInkThreshold
+    }
+
+    interface Callback {
+        fun updateEepaper(value: Int)
+    }
+
+}

+ 80 - 0
app/src/main/java/io/legato/kazusa/ui/book/manga/entities/EpaperTransformation.kt

@@ -0,0 +1,80 @@
+package io.legato.kazusa.ui.book.manga.entities
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import androidx.annotation.IntRange
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import java.security.MessageDigest
+
+/**
+ * 墨水屏图片转换器。
+ * 将彩色图片转换为灰度图,并可选择进行简单的二值化处理,以提高墨水屏显示效果。
+ *
+ * @param threshold 二值化的阈值(0-255)。低于此值的像素变为黑色,高于此值的像素变为白色。
+ *                  仅当applyBinarization为true时有效。
+ */
+class EpaperTransformation(
+    @IntRange(0, 255) private val threshold: Int = 128,
+) : BitmapTransformation() {
+
+    private val ID =
+        "io.legado.app.model.EpaperTransformation.${threshold}"
+    private val ID_BYTES = ID.toByteArray(CHARSET)
+
+    override fun transform(
+        pool: BitmapPool,
+        toTransform: Bitmap,
+        outWidth: Int,
+        outHeight: Int,
+    ): Bitmap {
+        val resultBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(resultBitmap)
+        val paint = Paint()
+
+        val colorMatrix = ColorMatrix()
+        colorMatrix.setSaturation(0f)
+        val filter = ColorMatrixColorFilter(colorMatrix)
+        paint.colorFilter = filter
+        canvas.drawBitmap(toTransform, 0f, 0f, paint)
+
+        val pixels = IntArray(outWidth * outHeight)
+        resultBitmap.getPixels(pixels, 0, outWidth, 0, 0, outWidth, outHeight)
+
+        for (i in pixels.indices) {
+            val pixel = pixels[i]
+            val gray = Color.red(pixel)
+            pixels[i] =
+                if (gray < threshold) Color.BLACK else Color.WHITE
+        }
+        resultBitmap.setPixels(pixels, 0, outWidth, 0, 0, outWidth, outHeight)
+
+        return resultBitmap
+    }
+
+    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
+        messageDigest.update(ID_BYTES)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as EpaperTransformation
+
+        if (threshold != other.threshold) return false
+        if (ID != other.ID) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = 31 + threshold
+        result = 31 * result + ID.hashCode()
+        return result
+    }
+}

+ 57 - 0
app/src/main/java/io/legato/kazusa/ui/book/manga/entities/GrayscaleTransformation.kt

@@ -0,0 +1,57 @@
+package io.legato.kazusa.ui.book.manga.entities
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+
+class GrayscaleTransformation : BitmapTransformation() {
+    private val ID = "io.legado.app.model.GrayscaleTransformation"
+
+    private val ID_BYTES = ID.toByteArray(StandardCharsets.UTF_8)
+
+    override fun transform(
+        pool: BitmapPool,
+        toTransform: Bitmap,
+        outWidth: Int,
+        outHeight: Int,
+    ): Bitmap {
+        val resultBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
+
+        val canvas = Canvas(resultBitmap)
+        val paint = Paint()
+
+        val matrix = ColorMatrix(
+            floatArrayOf(
+                0.299f, 0.587f, 0.114f, 0f, 0f,
+                0.299f, 0.587f, 0.114f, 0f, 0f,
+                0.299f, 0.587f, 0.114f, 0f, 0f,
+                0f, 0f, 0f, 1f, 0f
+            )
+        )
+        val filter = ColorMatrixColorFilter(matrix)
+        paint.colorFilter = filter
+        canvas.drawBitmap(toTransform, 0f, 0f, paint)
+        return resultBitmap
+    }
+
+    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
+        messageDigest.update(ID_BYTES)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        other as GrayscaleTransformation
+        return ID == other.ID
+    }
+
+    override fun hashCode(): Int {
+        return ID.hashCode()
+    }
+}

+ 39 - 5
app/src/main/java/io/legato/kazusa/ui/book/manga/recyclerview/MangaAdapter.kt

@@ -8,6 +8,7 @@ import android.view.LayoutInflater
 import android.view.ViewGroup
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import androidx.annotation.IntRange
 import androidx.annotation.IntRange
+import androidx.core.util.size
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updateLayoutParams
 import androidx.recyclerview.widget.AsyncListDiffer
 import androidx.recyclerview.widget.AsyncListDiffer
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.DiffUtil
@@ -16,6 +17,7 @@ import androidx.viewbinding.ViewBinding
 import com.bumptech.glide.Glide
 import com.bumptech.glide.Glide
 import com.bumptech.glide.ListPreloader.PreloadModelProvider
 import com.bumptech.glide.ListPreloader.PreloadModelProvider
 import com.bumptech.glide.RequestBuilder
 import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
 import io.legato.kazusa.base.adapter.ItemViewHolder
 import io.legato.kazusa.base.adapter.ItemViewHolder
 import io.legato.kazusa.base.adapter.RecyclerAdapter.Companion.TYPE_FOOTER_VIEW
 import io.legato.kazusa.base.adapter.RecyclerAdapter.Companion.TYPE_FOOTER_VIEW
 import io.legato.kazusa.databinding.ItemBookMangaEdgeBinding
 import io.legato.kazusa.databinding.ItemBookMangaEdgeBinding
@@ -24,6 +26,8 @@ import io.legato.kazusa.help.glide.progress.ProgressManager
 import io.legato.kazusa.model.BookCover
 import io.legato.kazusa.model.BookCover
 import io.legato.kazusa.model.ReadManga
 import io.legato.kazusa.model.ReadManga
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaColorFilterConfig
+import io.legato.kazusa.ui.book.manga.entities.EpaperTransformation
+import io.legato.kazusa.ui.book.manga.entities.GrayscaleTransformation
 import io.legato.kazusa.ui.book.manga.entities.MangaPage
 import io.legato.kazusa.ui.book.manga.entities.MangaPage
 import io.legato.kazusa.ui.book.manga.entities.ReaderLoading
 import io.legato.kazusa.ui.book.manga.entities.ReaderLoading
 import io.legato.kazusa.utils.dpToPx
 import io.legato.kazusa.utils.dpToPx
@@ -34,6 +38,8 @@ class MangaAdapter(private val context: Context) :
 
 
     private val inflater: LayoutInflater = LayoutInflater.from(context)
     private val inflater: LayoutInflater = LayoutInflater.from(context)
     private lateinit var mConfig: MangaColorFilterConfig
     private lateinit var mConfig: MangaColorFilterConfig
+    private var mTransformation: BitmapTransformation? = null
+    private var currentMangaEInkThreshold = 0
 
 
     companion object {
     companion object {
         private const val LOADING_VIEW = 0
         private const val LOADING_VIEW = 0
@@ -91,7 +97,7 @@ class MangaAdapter(private val context: Context) :
                 if (item is MangaPage) {
                 if (item is MangaPage) {
                     val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1
                     val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1
                     loadImageWithRetry(
                     loadImageWithRetry(
-                        item.mImageUrl, isHorizontal, isLastImage
+                        item.mImageUrl, isHorizontal, isLastImage, mTransformation
                     )
                     )
                 }
                 }
             }
             }
@@ -100,7 +106,7 @@ class MangaAdapter(private val context: Context) :
         fun onBind(item: MangaPage) {
         fun onBind(item: MangaPage) {
             setImageColorFilter()
             setImageColorFilter()
             val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1
             val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1
-            loadImageWithRetry(item.mImageUrl, isHorizontal, isLastImage)
+            loadImageWithRetry(item.mImageUrl, isHorizontal, isLastImage, mTransformation)
         }
         }
 
 
         fun setImageColorFilter() {
         fun setImageColorFilter() {
@@ -167,7 +173,7 @@ class MangaAdapter(private val context: Context) :
         }
         }
     }
     }
 
 
-    fun getFooterCount() = footerItems.size()
+    fun getFooterCount() = footerItems.size
 
 
     private fun isFooter(position: Int) = position >= getActualItemCount()
     private fun isFooter(position: Int) = position >= getActualItemCount()
 
 
@@ -199,8 +205,8 @@ class MangaAdapter(private val context: Context) :
     @Synchronized
     @Synchronized
     fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
     fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
         kotlin.runCatching {
         kotlin.runCatching {
-            val index = getActualItemCount() + footerItems.size()
-            footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
+            val index = getActualItemCount() + footerItems.size
+            footerItems.put(TYPE_FOOTER_VIEW + footerItems.size, footer)
             notifyItemInserted(index)
             notifyItemInserted(index)
         }
         }
     }
     }
@@ -244,4 +250,32 @@ class MangaAdapter(private val context: Context) :
         notifyItemRangeChanged(0, itemCount)
         notifyItemRangeChanged(0, itemCount)
     }
     }
 
 
+    fun enableMangaEInk(enable: Boolean, value: Int) {
+        if (enable) {
+            currentMangaEInkThreshold = value
+            mTransformation = EpaperTransformation(currentMangaEInkThreshold)
+        } else {
+            mTransformation = null
+        }
+        notifyItemRangeChanged(0, itemCount)
+    }
+
+    fun updateThreshold(mangaEInkThreshold: Int) {
+        if (currentMangaEInkThreshold != mangaEInkThreshold) {
+            currentMangaEInkThreshold = mangaEInkThreshold
+            mTransformation = EpaperTransformation(currentMangaEInkThreshold)
+            notifyItemRangeChanged(0, itemCount)
+        }
+    }
+
+    //开启灰色图片
+    fun enableGray(enable: Boolean) {
+        mTransformation = if (enable) {
+            GrayscaleTransformation()
+        } else {
+            null
+        }
+        notifyItemRangeChanged(0, itemCount)
+    }
+
 }
 }

+ 9 - 1
app/src/main/java/io/legato/kazusa/ui/book/manga/recyclerview/MangaVH.kt

@@ -2,6 +2,7 @@ package io.legato.kazusa.ui.book.manga.recyclerview
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context
+import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.Drawable
 import android.view.Gravity
 import android.view.Gravity
 import android.view.ViewGroup
 import android.view.ViewGroup
@@ -17,6 +18,7 @@ import androidx.core.view.updateLayoutParams
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewbinding.ViewBinding
 import androidx.viewbinding.ViewBinding
 import com.bumptech.glide.load.DataSource
 import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.Transformation
 import com.bumptech.glide.load.engine.GlideException
 import com.bumptech.glide.load.engine.GlideException
 import com.bumptech.glide.request.RequestListener
 import com.bumptech.glide.request.RequestListener
 import com.bumptech.glide.request.target.Target
 import com.bumptech.glide.request.target.Target
@@ -51,7 +53,12 @@ open class MangaVH<VB : ViewBinding>(val binding: VB, private val context: Conte
     }
     }
 
 
     @SuppressLint("CheckResult")
     @SuppressLint("CheckResult")
-    fun loadImageWithRetry(imageUrl: String, isHorizontal: Boolean, isLastImage: Boolean) {
+    fun loadImageWithRetry(
+        imageUrl: String,
+        isHorizontal: Boolean,
+        isLastImage: Boolean,
+        transformation: Transformation<Bitmap>?
+    ) {
         mFlProgress.isVisible = true
         mFlProgress.isVisible = true
         mLoading.isVisible = true
         mLoading.isVisible = true
         mRetry?.isGone = true
         mRetry?.isGone = true
@@ -67,6 +74,7 @@ open class MangaVH<VB : ViewBinding>(val binding: VB, private val context: Conte
                 context,
                 context,
                 imageUrl,
                 imageUrl,
                 sourceOrigin = ReadManga.book?.origin,
                 sourceOrigin = ReadManga.book?.origin,
+                transformation = transformation
             ).addListener(object : RequestListener<Drawable> {
             ).addListener(object : RequestListener<Drawable> {
                 override fun onLoadFailed(
                 override fun onLoadFailed(
                     e: GlideException?,
                     e: GlideException?,

+ 1 - 1
app/src/main/res/layout/dialog_manga_color_filter.xml

@@ -14,7 +14,7 @@
             android:layout_width="wrap_content"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:paddingBottom="8dp"
             android:paddingBottom="8dp"
-            android:text="滤镜"
+            android:text="@string/manga_color_filter"
             android:textSize="18sp" />
             android:textSize="18sp" />
 
 
         <io.legato.kazusa.ui.widget.DetailSeekBar
         <io.legato.kazusa.ui.widget.DetailSeekBar

+ 29 - 0
app/src/main/res/layout/dialog_manga_epaper.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingBottom="8dp"
+            android:text="@string/manga_epaper_stting"
+            android:textSize="18sp" />
+
+        <io.legato.kazusa.ui.widget.DetailSeekBar
+            android:id="@+id/dsb_epaper"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:max="255"
+            app:title="@string/manga_epaper_value" />
+
+    </LinearLayout>
+
+</androidx.core.widget.NestedScrollView>

+ 43 - 12
app/src/main/res/layout/dialog_update.xml

@@ -98,20 +98,51 @@
         </com.google.android.material.card.MaterialCardView>
         </com.google.android.material.card.MaterialCardView>
     </LinearLayout>
     </LinearLayout>
 
 
-    <com.google.android.material.card.MaterialCardView
-        android:layout_width="wrap_content"
+    <LinearLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
-        app:strokeWidth="0dp"
-        app:cardBackgroundColor="?attr/colorPrimaryContainer">
-        <TextView
-            android:id="@+id/tv_abi"
-            android:layout_width="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="horizontal">
+
+        <com.google.android.material.card.MaterialCardView
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
-            android:padding="8dp"
-            android:text="ABI: arm64-v8a"
-            android:textColor="?attr/colorOnSurfaceVariant"
-            android:textAppearance="?attr/textAppearanceBodySmall" />
-    </com.google.android.material.card.MaterialCardView>
+            app:cardBackgroundColor="?attr/colorPrimaryContainer"
+            app:strokeWidth="0dp">
+
+            <TextView
+                android:id="@+id/tv_abi"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="8dp"
+                android:text="ABI: arm64-v8a"
+                android:textAppearance="?attr/textAppearanceBodySmall"
+                android:textColor="?attr/colorOnSurfaceVariant" />
+        </com.google.android.material.card.MaterialCardView>
+
+        <View
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_marginHorizontal="8dp"
+            android:layout_weight="1"
+            android:background="?attr/colorSecondary" />
+
+        <com.google.android.material.card.MaterialCardView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:cardBackgroundColor="?attr/colorPrimaryContainer"
+            app:strokeWidth="0dp">
+
+            <TextView
+                android:id="@+id/tv_variable"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:padding="8dp"
+                android:text="123"
+                android:textAppearance="?attr/textAppearanceBodyMedium" />
+        </com.google.android.material.card.MaterialCardView>
+    </LinearLayout>
+
 
 
     <TextView
     <TextView
         android:id="@+id/tv_url"
         android:id="@+id/tv_url"

+ 28 - 0
app/src/main/res/menu/book_manga.xml

@@ -71,6 +71,14 @@
         android:title="@string/enable_manga_horizontal_scroll"
         android:title="@string/enable_manga_horizontal_scroll"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
+    <item
+        android:id="@+id/menu_disable_horizontal_animation"
+        android:checkable="true"
+        android:checked="false"
+        android:title="@string/disable_horizontal_animation"
+        android:visible="false"
+        app:showAsAction="never" />
+
     <item
     <item
         android:id="@+id/menu_manga_footer_config"
         android:id="@+id/menu_manga_footer_config"
         android:title="@string/manga_footer_config"
         android:title="@string/manga_footer_config"
@@ -88,4 +96,24 @@
         android:title="@string/hide_manga_title"
         android:title="@string/hide_manga_title"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
+    <item
+        android:id="@+id/menu_epaper_manga"
+        android:checkable="true"
+        android:checked="false"
+        android:title="@string/manga_epaper"
+        app:showAsAction="never" />
+
+    <item
+        android:id="@+id/menu_epaper_manga_setting"
+        android:title="@string/manga_epaper_stting"
+        android:visible="false"
+        app:showAsAction="never" />
+
+    <item
+        android:id="@+id/menu_gray_manga"
+        android:checkable="true"
+        android:checked="false"
+        android:title="@string/enable_manga_gray"
+        app:showAsAction="never" />
+
 </menu>
 </menu>

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

@@ -1217,8 +1217,18 @@
     <string name="manga_color_filter">滤镜</string>
     <string name="manga_color_filter">滤镜</string>
     <string name="hide_manga_title">隐藏漫画列表标题</string>
     <string name="hide_manga_title">隐藏漫画列表标题</string>
     <string name="refresh_explore">刷新发现</string>
     <string name="refresh_explore">刷新发现</string>
+    <string name="ungroup">未分组</string>
+    <string name="dynamic_colors">动态取色</string>
     <string name="change_icon_success">图标已修改</string>
     <string name="change_icon_success">图标已修改</string>
+    <string name="replace_rule_title_setting">配置替换规则</string>
+    <string name="more_setting">更多设置</string>
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_index">已读 %d 章</string>
     <string name="read_chapter_index">已读 %d 章</string>
 
 
+    <string name="manga_epaper">墨水屏</string>
+    <string name="manga_epaper_stting">墨水屏设置</string>
+    <string name="manga_epaper_value">阈值</string>
+    <string name="disable_horizontal_animation">禁用水平滚动动画</string>
+    <string name="enable_manga_gray">开启图片灰色</string>
+
 </resources>
 </resources>

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

@@ -1226,4 +1226,10 @@
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_index">已读 %d 章</string>
     <string name="read_chapter_index">已读 %d 章</string>
 
 
+    <string name="manga_epaper">墨水屏</string>
+    <string name="manga_epaper_stting">墨水屏设置</string>
+    <string name="manga_epaper_value">阈值</string>
+    <string name="disable_horizontal_animation">禁用水平滚动动画</string>
+    <string name="enable_manga_gray">开启图片灰色</string>
+
 </resources>
 </resources>