新增漫画可保存图片

This commit is contained in:
HapeLee
2025-09-03 04:12:20 +08:00
parent f9ee992642
commit 17e16958a3
6 changed files with 116 additions and 51 deletions
@@ -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)
}
/**
* 调整漫画类型
@@ -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
@@ -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
@@ -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
@@ -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,
)
@@ -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()
}
}