新增漫画可保存图片

This commit is contained in:
HapeLee committed 2025-09-03 04:12:20 +08:00
1 parent f9ee992642
commit 17e16958a3
6 files changed
+116 -51

No files matched your search

@@ -39,19 +39,21 @@ import io.legato.kazusa.help.book.isImage
import io.legato.kazusa.help.book.removeType import io.legato.kazusa.help.book.removeType
import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.storage.Backup 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.alert
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.model.ReadManga import io.legato.kazusa.model.ReadManga
import io.legato.kazusa.receiver.NetworkChangedListener import io.legato.kazusa.receiver.NetworkChangedListener
import io.legato.kazusa.ui.book.changesource.ChangeBookSourceDialog 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.MangaAutoReadDialog
import io.legato.kazusa.ui.book.manga.config.MangaClickActionConfigDialog 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.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.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.config.MangaScrollMode 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.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.entities.MangaPage
import io.legato.kazusa.ui.book.manga.recyclerview.MangaAdapter import io.legato.kazusa.ui.book.manga.recyclerview.MangaAdapter
import io.legato.kazusa.ui.book.manga.recyclerview.MangaLayoutManager import io.legato.kazusa.ui.book.manga.recyclerview.MangaLayoutManager
@@ -85,7 +87,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, MangaFooterSettingDialog.Callback, MangaColorFilterDialog.Callback, ScrollTimer.ScrollCallback, MangaFooterSettingDialog.Callback,
MangaScrollModeDialog.Callback { MangaAutoReadDialog.Callback {
private val mLayoutManager by lazy { private val mLayoutManager by lazy {
MangaLayoutManager(this) MangaLayoutManager(this)
@@ -161,21 +163,16 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
window.sharedElementEnterTransition = MaterialContainerTransform().apply { val transform = MaterialContainerTransform().apply {
addTarget(binding.root) addTarget(binding.rootView)
duration = 700
scrimColor = Color.TRANSPARENT scrimColor = Color.TRANSPARENT
fadeMode = MaterialContainerTransform.FADE_MODE_THROUGH duration = 500
}
window.sharedElementReturnTransition = MaterialContainerTransform().apply {
addTarget(binding.root)
duration = 700
scrimColor = Color.BLACK
fadeMode = MaterialContainerTransform.FADE_MODE_THROUGH
} }
window.sharedElementEnterTransition = transform
window.sharedElementReturnTransition = transform
upLayoutInDisplayCutoutMode() upLayoutInDisplayCutoutMode()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding.root.transitionName = intent.getStringExtra("transitionName") binding.rootView.transitionName = intent.getStringExtra("transitionName")
ReadManga.register(this) ReadManga.register(this)
upSystemUiVisibility(false) upSystemUiVisibility(false)
initRecyclerView() 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) { override fun onNewIntent(intent: Intent) {
@@ -585,7 +602,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
} }
override fun showScrollModeDialog() { override fun showScrollModeDialog() {
showDialogFragment(MangaScrollModeDialog().apply { showDialogFragment(MangaAutoReadDialog().apply {
initialAutoPageEnabled = enableScroll initialAutoPageEnabled = enableScroll
callback = this@ReadMangaActivity callback = this@ReadMangaActivity
}) })
@@ -638,9 +655,9 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
} }
//自动翻页 //自动翻页
override fun onAutoPageToggle(enabled: Boolean) { override fun onAutoPageToggle(enable: Boolean) {
enableScroll = enabled enableScroll = enable
setAutoReadEnabled(enabled) setAutoReadEnabled(enable)
} }
//自动翻页速度 //自动翻页速度
@@ -648,7 +665,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
setAutoReadEnabled(false) setAutoReadEnabled(false)
AppConfig.mangaAutoPageSpeed = speed AppConfig.mangaAutoPageSpeed = speed
mScrollTimer.setSpeed(speed) mScrollTimer.setSpeed(speed)
setAutoReadEnabled(true) setAutoReadEnabled(enableScroll)
// if (enableAutoScrollPage) { // if (enableAutoScrollPage) {
// mScrollTimer.isEnabledPage = true // 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.app.Application
import android.content.Intent 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.R
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppLog 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.ReadManga
import io.legato.kazusa.model.localBook.LocalBook import io.legato.kazusa.model.localBook.LocalBook
import io.legato.kazusa.model.webBook.WebBook import io.legato.kazusa.model.webBook.WebBook
import io.legato.kazusa.utils.ImageSaveUtils
import io.legato.kazusa.utils.mapParallelSafe import io.legato.kazusa.utils.mapParallelSafe
import io.legato.kazusa.utils.postEvent import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.toastOnUi import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -35,7 +40,10 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onEmpty import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.ByteArrayOutputStream
class ReadMangaViewModel(application: Application) : BaseViewModel(application) { class ReadMangaViewModel(application: Application) : BaseViewModel(application) {
@@ -283,6 +291,39 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
return ReadManga.book?.readConfig?.webtoonSidePaddingDp ?: AppConfig.webtoonSidePaddingDp 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) { fun updateReadConfig(update: ReadConfig.() -> Unit) {
val book = ReadManga.book ?: return val book = ReadManga.book ?: return
@@ -4,13 +4,13 @@ import android.os.Bundle
import android.view.View import android.view.View
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.databinding.DialogMangaScrollModeBinding import io.legato.kazusa.databinding.DialogMangaAutoReadBinding
import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding 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 var initialAutoPageEnabled: Boolean = false
@@ -4,12 +4,15 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
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.EventBus import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.databinding.DialogMangaFooterSettingBinding import io.legato.kazusa.databinding.DialogMangaFooterSettingBinding
import io.legato.kazusa.help.config.AppConfig 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.ui.widget.ReaderInfoBarView
import io.legato.kazusa.utils.GSON import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject import io.legato.kazusa.utils.fromJsonObject
@@ -46,10 +49,17 @@ class MangaFooterSettingDialog :
setOnClickListener { setOnClickListener {
callback?.onScrollModeChanged(mode) 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 { binding.scvPadding.apply {
valueFormat = { "$it %" } valueFormat = { "$it %" }
progress = initialWebtoonSidePadding progress = initialWebtoonSidePadding
@@ -200,7 +210,6 @@ class MangaFooterSettingDialog :
} }
private fun updateChapterText() { private fun updateChapterText() {
if (config.hideFooter) { if (config.hideFooter) {
binding.tvChapter.visibility = View.INVISIBLE binding.tvChapter.visibility = View.INVISIBLE
return 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 androidx.annotation.Keep
import io.legato.kazusa.ui.widget.ReaderInfoBarView import io.legato.kazusa.ui.widget.ReaderInfoBarView
@@ -11,7 +11,7 @@ data class MangaFooterConfig(
var hidePageNumber: Boolean = false, var hidePageNumber: Boolean = false,
var hideProgressRatioLabel: Boolean = false, var hideProgressRatioLabel: Boolean = false,
var hideProgressRatio: Boolean = false, var hideProgressRatio: Boolean = false,
var footerOrientation: Int = ReaderInfoBarView.ALIGN_LEFT,//默认靠左 var footerOrientation: Int = ReaderInfoBarView.Companion.ALIGN_LEFT,//默认靠左
var hideFooter: Boolean = false, var hideFooter: Boolean = false,
var hideChapterName:Boolean=false, var hideChapterName:Boolean=false,
) )
@@ -8,7 +8,9 @@ import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView
import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.utils.findCenterViewPosition
class WebtoonFrame : FrameLayout { class WebtoonFrame : FrameLayout {
@@ -20,6 +22,9 @@ class WebtoonFrame : FrameLayout {
defStyle defStyle
) )
/** 长按回调,返回手指坐标 */
var longPressListener: ((centerPosition: Int) -> Unit)? = null
private val scaleDetector = ScaleGestureDetector(context, ScaleListener()) private val scaleDetector = ScaleGestureDetector(context, ScaleListener())
private val flingDetector = GestureDetector(context, FlingListener()) private val flingDetector = GestureDetector(context, FlingListener())
private val regionRects = mutableMapOf<String, RectF>() private val regionRects = mutableMapOf<String, RectF>()
@@ -37,9 +42,22 @@ class WebtoonFrame : FrameLayout {
get() = getChildAt(0) as? WebtoonRecyclerView get() = getChildAt(0) as? WebtoonRecyclerView
var disabledClickScroll = false var disabledClickScroll = false
var actionHandler: ClickActionHandler? = null 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() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
recycler?.tapListener = tapListener@{ ev -> recycler?.tapListener = tapListener@{ ev ->
@@ -63,7 +81,6 @@ class WebtoonFrame : FrameLayout {
performClickAction(action) performClickAction(action)
} }
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -84,8 +101,9 @@ class WebtoonFrame : FrameLayout {
} }
} }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
gestureDetector.onTouchEvent(ev) // 捕获长按
if (!disableMangaScale) { if (!disableMangaScale) {
scaleDetector.onTouchEvent(ev) scaleDetector.onTouchEvent(ev)
flingDetector.onTouchEvent(ev) flingDetector.onTouchEvent(ev)
@@ -99,7 +117,7 @@ class WebtoonFrame : FrameLayout {
ev.setLocation( ev.setLocation(
ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()), 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) return super.dispatchTouchEvent(ev)
@@ -134,15 +152,13 @@ class WebtoonFrame : FrameLayout {
} }
inner class FlingListener : GestureDetector.SimpleOnGestureListener() { inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean { override fun onDown(e: MotionEvent): Boolean = true
return true
}
override fun onFling( override fun onFling(
e1: MotionEvent?, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float, velocityY: Float
): Boolean { ): Boolean {
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
} }
@@ -155,5 +171,4 @@ class WebtoonFrame : FrameLayout {
fun nextChapter() fun nextChapter()
fun prevChapter() fun prevChapter()
} }
} }