Sfoglia il codice sorgente

新增漫画可保存图片

HapeLee 1 anno fa
parent
commit
17e16958a3

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

@@ -39,19 +39,21 @@ import io.legato.kazusa.help.book.isImage
 import io.legato.kazusa.help.book.removeType
 import io.legato.kazusa.help.config.AppConfig
 import io.legato.kazusa.help.storage.Backup
+import io.legato.kazusa.lib.dialogs.SelectItem
 import io.legato.kazusa.lib.dialogs.alert
+import io.legato.kazusa.lib.dialogs.selector
 import io.legato.kazusa.model.ReadManga
 import io.legato.kazusa.receiver.NetworkChangedListener
 import io.legato.kazusa.ui.book.changesource.ChangeBookSourceDialog
 import io.legato.kazusa.ui.book.info.BookInfoActivity
+import io.legato.kazusa.ui.book.manga.config.MangaAutoReadDialog
 import io.legato.kazusa.ui.book.manga.config.MangaClickActionConfigDialog
 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.MangaFooterConfig
 import io.legato.kazusa.ui.book.manga.config.MangaFooterSettingDialog
 import io.legato.kazusa.ui.book.manga.config.MangaScrollMode
-import io.legato.kazusa.ui.book.manga.config.MangaScrollModeDialog
 import io.legato.kazusa.ui.book.manga.entities.BaseMangaPage
+import io.legato.kazusa.ui.book.manga.entities.MangaFooterConfig
 import io.legato.kazusa.ui.book.manga.entities.MangaPage
 import io.legato.kazusa.ui.book.manga.recyclerview.MangaAdapter
 import io.legato.kazusa.ui.book.manga.recyclerview.MangaLayoutManager
@@ -85,7 +87,7 @@ import kotlin.math.ceil
 class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewModel>(),
     ReadManga.Callback, ChangeBookSourceDialog.CallBack, MangaMenu.CallBack,
     MangaColorFilterDialog.Callback, ScrollTimer.ScrollCallback, MangaFooterSettingDialog.Callback,
-    MangaScrollModeDialog.Callback {
+    MangaAutoReadDialog.Callback {
 
     private val mLayoutManager by lazy {
         MangaLayoutManager(this)
@@ -161,21 +163,16 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        window.sharedElementEnterTransition = MaterialContainerTransform().apply {
-            addTarget(binding.root)
-            duration = 700
+        val transform = MaterialContainerTransform().apply {
+            addTarget(binding.rootView)
             scrimColor = Color.TRANSPARENT
-            fadeMode = MaterialContainerTransform.FADE_MODE_THROUGH
-        }
-        window.sharedElementReturnTransition = MaterialContainerTransform().apply {
-            addTarget(binding.root)
-            duration = 700
-            scrimColor = Color.BLACK
-            fadeMode = MaterialContainerTransform.FADE_MODE_THROUGH
+            duration = 500
         }
+        window.sharedElementEnterTransition = transform
+        window.sharedElementReturnTransition = transform
         upLayoutInDisplayCutoutMode()
         super.onCreate(savedInstanceState)
-        binding.root.transitionName = intent.getStringExtra("transitionName")
+        binding.rootView.transitionName = intent.getStringExtra("transitionName")
         ReadManga.register(this)
         upSystemUiVisibility(false)
         initRecyclerView()
@@ -279,6 +276,26 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
             }
         }
 
+        binding.webtoonFrame.longPressListener = {
+            val centerPos = binding.recyclerView.findCenterViewPosition()
+            val currentItem = mAdapter.getItem(centerPos)
+            val page = currentItem as? MangaPage
+            page?.let { item ->
+                selector(
+                    arrayListOf(
+                        SelectItem("保存", "save"),
+                        SelectItem("分享", "share")
+                    )
+                ) { _, option, _ ->
+                    when (option.value) {
+                        "save" -> viewModel.saveImageToGallery(item.mImageUrl)
+                        // "share" -> shareImage(item.mImageUrl)
+                    }
+                }
+            }
+        }
+
+
     }
 
     override fun onNewIntent(intent: Intent) {
@@ -585,7 +602,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
     }
 
     override fun showScrollModeDialog() {
-        showDialogFragment(MangaScrollModeDialog().apply {
+        showDialogFragment(MangaAutoReadDialog().apply {
             initialAutoPageEnabled = enableScroll
             callback = this@ReadMangaActivity
         })
@@ -638,9 +655,9 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
     }
 
     //自动翻页
-    override fun onAutoPageToggle(enabled: Boolean) {
-        enableScroll = enabled
-        setAutoReadEnabled(enabled)
+    override fun onAutoPageToggle(enable: Boolean) {
+        enableScroll = enable
+        setAutoReadEnabled(enable)
     }
 
     //自动翻页速度
@@ -648,7 +665,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         setAutoReadEnabled(false)
         AppConfig.mangaAutoPageSpeed = speed
         mScrollTimer.setSpeed(speed)
-        setAutoReadEnabled(true)
+        setAutoReadEnabled(enableScroll)
 //        if (enableAutoScrollPage) {
 //            mScrollTimer.isEnabledPage = true
 //        }
@@ -708,23 +725,6 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         }
     }
 
-    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
-        val keyCode = event.keyCode
-        val action = event.action
-        val isDown = action == 0
-
-        if (keyCode == KeyEvent.KEYCODE_MENU) {
-            if (isDown && !binding.mangaMenu.canShowMenu) {
-                binding.mangaMenu.runMenuIn()
-                return true
-            }
-            if (!isDown && !binding.mangaMenu.canShowMenu) {
-                binding.mangaMenu.canShowMenu = true
-                return true
-            }
-        }
-        return super.dispatchKeyEvent(event)
-    }
 
     /**
      * 调整漫画类型

+ 41 - 0
app/src/main/java/io/legato/kazusa/ui/book/manga/ReadMangaViewModel.kt

@@ -2,6 +2,9 @@ package io.legato.kazusa.ui.book.manga
 
 import android.app.Application
 import android.content.Intent
+import android.graphics.Bitmap
+import androidx.lifecycle.viewModelScope
+import com.bumptech.glide.Glide
 import io.legato.kazusa.R
 import io.legato.kazusa.base.BaseViewModel
 import io.legato.kazusa.constant.AppLog
@@ -24,9 +27,11 @@ import io.legato.kazusa.help.coroutine.Coroutine
 import io.legato.kazusa.model.ReadManga
 import io.legato.kazusa.model.localBook.LocalBook
 import io.legato.kazusa.model.webBook.WebBook
+import io.legato.kazusa.utils.ImageSaveUtils
 import io.legato.kazusa.utils.mapParallelSafe
 import io.legato.kazusa.utils.postEvent
 import io.legato.kazusa.utils.toastOnUi
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flow
@@ -35,7 +40,10 @@ import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onEmpty
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import splitties.init.appCtx
+import java.io.ByteArrayOutputStream
 
 class ReadMangaViewModel(application: Application) : BaseViewModel(application) {
 
@@ -283,6 +291,39 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
         return ReadManga.book?.readConfig?.webtoonSidePaddingDp ?: AppConfig.webtoonSidePaddingDp
     }
 
+    fun saveImageToGallery(url: String, folderName: String = "Legado") {
+        viewModelScope.launch {
+            try {
+                val byteArray = withContext(Dispatchers.IO) {
+                    // 用 Glide 下载 Bitmap 转换成 ByteArray
+                    val bitmap = Glide.with(getApplication<Application>().applicationContext)
+                        .asBitmap()
+                        .load(url)
+                        .submit()
+                        .get()
+                    val baos = ByteArrayOutputStream()
+                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
+                    baos.toByteArray()
+                }
+
+                val context = getApplication<Application>().applicationContext
+                val success = ImageSaveUtils.saveImageToGallery(
+                    context,
+                    byteArray,
+                    folderName = folderName
+                )
+
+                if (success) {
+                    context.toastOnUi("图片已保存到相册")
+                } else {
+                    context.toastOnUi("保存失败")
+                }
+            } catch (e: Exception) {
+                context.toastOnUi("保存出错: ${e.localizedMessage}")
+            }
+        }
+    }
+
 
     fun updateReadConfig(update: ReadConfig.() -> Unit) {
         val book = ReadManga.book ?: return

+ 3 - 3
app/src/main/java/io/legato/kazusa/ui/book/manga/config/MangaScrollModeDialog.kt → app/src/main/java/io/legato/kazusa/ui/book/manga/config/MangaAutoReadDialog.kt

@@ -4,13 +4,13 @@ import android.os.Bundle
 import android.view.View
 import io.legato.kazusa.R
 import io.legato.kazusa.base.BaseBottomSheetDialogFragment
-import io.legato.kazusa.databinding.DialogMangaScrollModeBinding
+import io.legato.kazusa.databinding.DialogMangaAutoReadBinding
 import io.legato.kazusa.help.config.AppConfig
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
 
-class MangaScrollModeDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_scroll_mode) {
+class MangaAutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_auto_read) {
 
-    private val binding by viewBinding(DialogMangaScrollModeBinding::bind)
+    private val binding by viewBinding(DialogMangaAutoReadBinding::bind)
 
     var initialAutoPageEnabled: Boolean = false
 

+ 10 - 1
app/src/main/java/io/legato/kazusa/ui/book/manga/config/MangaFooterSettingDialog.kt

@@ -4,12 +4,15 @@ import android.content.DialogInterface
 import android.os.Bundle
 import android.view.Gravity
 import android.view.View
+import androidx.core.view.isVisible
+import androidx.transition.TransitionManager
 import com.google.android.material.chip.Chip
 import io.legato.kazusa.R
 import io.legato.kazusa.base.BaseBottomSheetDialogFragment
 import io.legato.kazusa.constant.EventBus
 import io.legato.kazusa.databinding.DialogMangaFooterSettingBinding
 import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.ui.book.manga.entities.MangaFooterConfig
 import io.legato.kazusa.ui.widget.ReaderInfoBarView
 import io.legato.kazusa.utils.GSON
 import io.legato.kazusa.utils.fromJsonObject
@@ -46,10 +49,17 @@ class MangaFooterSettingDialog :
 
                 setOnClickListener {
                     callback?.onScrollModeChanged(mode)
+                    TransitionManager.beginDelayedTransition(binding.rootView)
+                    binding.llWebtoon.isVisible = mode == MangaScrollMode.WEBTOON ||
+                            mode == MangaScrollMode.WEBTOON_WITH_GAP
                 }
             })
         }
 
+        binding.llWebtoon.isVisible = initialScrollMode == MangaScrollMode.WEBTOON ||
+                initialScrollMode == MangaScrollMode.WEBTOON_WITH_GAP
+
+
         binding.scvPadding.apply {
             valueFormat = { "$it %" }
             progress = initialWebtoonSidePadding
@@ -200,7 +210,6 @@ class MangaFooterSettingDialog :
     }
 
     private fun updateChapterText() {
-
         if (config.hideFooter) {
             binding.tvChapter.visibility = View.INVISIBLE
             return

+ 2 - 2
app/src/main/java/io/legato/kazusa/ui/book/manga/config/MangaFooterConfig.kt → app/src/main/java/io/legato/kazusa/ui/book/manga/entities/MangaFooterConfig.kt

@@ -1,4 +1,4 @@
-package io.legato.kazusa.ui.book.manga.config
+package io.legato.kazusa.ui.book.manga.entities
 
 import androidx.annotation.Keep
 import io.legato.kazusa.ui.widget.ReaderInfoBarView
@@ -11,7 +11,7 @@ data class MangaFooterConfig(
     var hidePageNumber: Boolean = false,
     var hideProgressRatioLabel: Boolean = false,
     var hideProgressRatio: Boolean = false,
-    var footerOrientation: Int = ReaderInfoBarView.ALIGN_LEFT,//默认靠左
+    var footerOrientation: Int = ReaderInfoBarView.Companion.ALIGN_LEFT,//默认靠左
     var hideFooter: Boolean = false,
     var hideChapterName:Boolean=false,
 )

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

@@ -8,7 +8,9 @@ import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.ScaleGestureDetector
 import android.widget.FrameLayout
+import androidx.recyclerview.widget.RecyclerView
 import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.utils.findCenterViewPosition
 
 class WebtoonFrame : FrameLayout {
 
@@ -20,6 +22,9 @@ class WebtoonFrame : FrameLayout {
         defStyle
     )
 
+    /** 长按回调,返回手指坐标 */
+    var longPressListener: ((centerPosition: Int) -> Unit)? = null
+
     private val scaleDetector = ScaleGestureDetector(context, ScaleListener())
     private val flingDetector = GestureDetector(context, FlingListener())
     private val regionRects = mutableMapOf<String, RectF>()
@@ -37,9 +42,22 @@ class WebtoonFrame : FrameLayout {
         get() = getChildAt(0) as? WebtoonRecyclerView
 
     var disabledClickScroll = false
-
     var actionHandler: ClickActionHandler? = null
 
+    private val gestureDetector =
+        GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
+            override fun onLongPress(e: MotionEvent) {
+                recycler?.let {
+                    val centerPos = it.findCenterViewPosition()
+                    if (centerPos != RecyclerView.NO_POSITION) {
+                        longPressListener?.invoke(centerPos)
+                    }
+                }
+            }
+
+            override fun onDown(e: MotionEvent): Boolean = true
+        })
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         recycler?.tapListener = tapListener@{ ev ->
@@ -63,7 +81,6 @@ class WebtoonFrame : FrameLayout {
 
             performClickAction(action)
         }
-
     }
 
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -84,8 +101,9 @@ class WebtoonFrame : FrameLayout {
         }
     }
 
-
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        gestureDetector.onTouchEvent(ev)  // 捕获长按
+
         if (!disableMangaScale) {
             scaleDetector.onTouchEvent(ev)
             flingDetector.onTouchEvent(ev)
@@ -99,7 +117,7 @@ class WebtoonFrame : FrameLayout {
 
             ev.setLocation(
                 ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()),
-                ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat()),
+                ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat())
             )
         }
         return super.dispatchTouchEvent(ev)
@@ -134,15 +152,13 @@ class WebtoonFrame : FrameLayout {
     }
 
     inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
-        override fun onDown(e: MotionEvent): Boolean {
-            return true
-        }
+        override fun onDown(e: MotionEvent): Boolean = true
 
         override fun onFling(
             e1: MotionEvent?,
             e2: MotionEvent,
             velocityX: Float,
-            velocityY: Float,
+            velocityY: Float
         ): Boolean {
             return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
         }
@@ -155,5 +171,4 @@ class WebtoonFrame : FrameLayout {
         fun nextChapter()
         fun prevChapter()
     }
-
 }