使用MediaStore保存图片

This commit is contained in:
HapeLee committed 2025-09-03 04:13:28 +08:00
1 parent ab6f7bc780
commit deb945dfbc
7 files changed
+150 -177

No files matched your search

@@ -19,9 +19,6 @@ import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toUri
import androidx.core.view.doOnLayout
import androidx.core.view.doOnPreDraw
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.size import androidx.core.view.size
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -30,7 +27,6 @@ import com.google.android.material.transition.platform.MaterialSharedAxis
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legato.kazusa.BuildConfig import io.legato.kazusa.BuildConfig
import io.legato.kazusa.R import io.legato.kazusa.R
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.BookType import io.legato.kazusa.constant.BookType
import io.legato.kazusa.constant.EventBus import io.legato.kazusa.constant.EventBus
@@ -99,13 +95,11 @@ import io.legato.kazusa.ui.book.toc.TocActivityResult
import io.legato.kazusa.ui.book.toc.rule.TxtTocRuleDialog import io.legato.kazusa.ui.book.toc.rule.TxtTocRuleDialog
import io.legato.kazusa.ui.browser.WebViewActivity import io.legato.kazusa.ui.browser.WebViewActivity
import io.legato.kazusa.ui.dict.DictDialog import io.legato.kazusa.ui.dict.DictDialog
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.login.SourceLoginActivity import io.legato.kazusa.ui.login.SourceLoginActivity
import io.legato.kazusa.ui.replace.ReplaceRuleActivity import io.legato.kazusa.ui.replace.ReplaceRuleActivity
import io.legato.kazusa.ui.replace.edit.ReplaceEditActivity import io.legato.kazusa.ui.replace.edit.ReplaceEditActivity
import io.legato.kazusa.ui.widget.PopupAction import io.legato.kazusa.ui.widget.PopupAction
import io.legato.kazusa.ui.widget.dialog.PhotoDialog import io.legato.kazusa.ui.widget.dialog.PhotoDialog
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.Debounce import io.legato.kazusa.utils.Debounce
import io.legato.kazusa.utils.LogUtils import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.NetworkUtils import io.legato.kazusa.utils.NetworkUtils
@@ -120,7 +114,6 @@ import io.legato.kazusa.utils.iconItemOnLongClick
import io.legato.kazusa.utils.invisible import io.legato.kazusa.utils.invisible
import io.legato.kazusa.utils.isAbsUrl import io.legato.kazusa.utils.isAbsUrl
import io.legato.kazusa.utils.isTrue import io.legato.kazusa.utils.isTrue
import io.legato.kazusa.utils.launch
import io.legato.kazusa.utils.navigationBarGravity import io.legato.kazusa.utils.navigationBarGravity
import io.legato.kazusa.utils.observeEvent import io.legato.kazusa.utils.observeEvent
import io.legato.kazusa.utils.observeEventSticky import io.legato.kazusa.utils.observeEventSticky
@@ -212,12 +205,6 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.loadOrUpContent() ReadBook.loadOrUpContent()
} }
} }
private val selectImageDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
ACache.get().put(AppConst.imagePathKey, uri.toString())
viewModel.saveImage(it.value, uri)
}
}
private var menu: Menu? = null private var menu: Menu? = null
private var backupJob: Job? = null private var backupJob: Job? = null
private var tts: TTS? = null private var tts: TTS? = null
@@ -267,7 +254,7 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (AppConfig.sharedElementEnterTransitionEnable) { if (AppConfig.sharedElementEnterTransitionEnable) {
postponeEnterTransition()
val transform = MaterialContainerTransform().apply { val transform = MaterialContainerTransform().apply {
addTarget(binding.rootView) addTarget(binding.rootView)
scrimColor = Color.TRANSPARENT scrimColor = Color.TRANSPARENT
@@ -275,30 +262,33 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
window.sharedElementEnterTransition = transform window.sharedElementEnterTransition = transform
window.sharedElementReturnTransition =
MaterialSharedAxis(MaterialSharedAxis.X, true).apply {
duration = 300
}
if (AppConfig.delayBookLoadEnable) { if (AppConfig.delayBookLoadEnable) {
window.sharedElementEnterTransition?.addListener(object : window.sharedElementEnterTransition?.addListener(object :
Transition.TransitionListener { Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) {} override fun onTransitionStart(transition: Transition) {}
override fun onTransitionEnd(transition: Transition) { override fun onTransitionEnd(transition: Transition) {
binding.readView.doOnLayout { lifecycleScope.launch(Dispatchers.Default) {
viewModel.initReadBookConfig(intent) viewModel.initReadBookConfig(intent)
viewModel.initData(intent) viewModel.initData(intent)
binding.readView.initAfterTransition()
justInitData = true withContext(Main) {
binding.readView.initAfterTransition()
justInitData = true
}
} }
transition.removeListener(this) transition.removeListener(this)
} }
override fun onTransitionCancel(transition: Transition) { override fun onTransitionCancel(transition: Transition) {
viewModel.initReadBookConfig(intent) lifecycleScope.launch(Dispatchers.Default) {
viewModel.initData(intent) viewModel.initReadBookConfig(intent)
binding.readView.initAfterTransition() viewModel.initData(intent)
justInitData = true
withContext(Main) {
binding.readView.initAfterTransition()
justInitData = true
}
}
transition.removeListener(this) transition.removeListener(this)
} }
@@ -307,32 +297,21 @@ class ReadBookActivity : BaseReadBookActivity(),
}) })
} }
} }
window.sharedElementReturnTransition =
super.onCreate(savedInstanceState) MaterialSharedAxis(MaterialSharedAxis.X, true).apply {
duration = 300
if (!AppConfig.delayBookLoadEnable || !AppConfig.sharedElementEnterTransitionEnable) {
lifecycleScope.launch(Dispatchers.Default) {
viewModel.initReadBookConfig(intent)
viewModel.initData(intent)
withContext(Main) {
binding.readView.initAfterTransition()
justInitData = true
}
} }
} super.onCreate(savedInstanceState)
if (AppConfig.sharedElementEnterTransitionEnable) if (AppConfig.sharedElementEnterTransitionEnable)
binding.rootView.transitionName = intent.getStringExtra("transitionName") binding.rootView.transitionName = intent.getStringExtra("transitionName")
upScreenTimeOut()
ReadBook.register(this)
binding.cursorLeft.setOnTouchListener(this) binding.cursorLeft.setOnTouchListener(this)
binding.cursorRight.setOnTouchListener(this) binding.cursorRight.setOnTouchListener(this)
binding.rootView.doOnPreDraw {
startPostponedEnterTransition()
}
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (isShowingSearchResult) { if (isShowingSearchResult) {
exitSearchMenu() exitSearchMenu()
@@ -362,9 +341,19 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
upScreenTimeOut() if (!AppConfig.delayBookLoadEnable || !AppConfig.sharedElementEnterTransitionEnable) {
ReadBook.register(this) lifecycleScope.launch(Dispatchers.Default) {
viewModel.initReadBookConfig(intent)
viewModel.initData(intent)
withContext(Main) {
binding.readView.initAfterTransition()
justInitData = true
}
}
}
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
viewModel.initData(intent) viewModel.initData(intent)
@@ -1445,9 +1434,8 @@ class ReadBookActivity : BaseReadBookActivity(),
listOf( listOf(
SelectItem(getString(R.string.show), "show"), SelectItem(getString(R.string.show), "show"),
SelectItem(getString(R.string.refresh), "refresh"), SelectItem(getString(R.string.refresh), "refresh"),
SelectItem(getString(R.string.action_save), "save"), SelectItem("保存到相册", "save"),
SelectItem(getString(R.string.menu), "menu"), SelectItem(getString(R.string.menu), "menu"),
SelectItem(getString(R.string.select_folder), "selectFolder")
) )
) )
popupAction.onActionClick = { popupAction.onActionClick = {
@@ -1455,18 +1443,9 @@ class ReadBookActivity : BaseReadBookActivity(),
"show" -> showDialogFragment(PhotoDialog(src)) "show" -> showDialogFragment(PhotoDialog(src))
"refresh" -> viewModel.refreshImage(src) "refresh" -> viewModel.refreshImage(src)
"save" -> { "save" -> {
val path = ACache.get().getAsString(AppConst.imagePathKey) viewModel.saveImage(src)
if (path.isNullOrEmpty()) {
selectImageDir.launch {
value = src
}
} else {
viewModel.saveImage(src, path.toUri())
}
} }
"menu" -> showActionMenu() "menu" -> showActionMenu()
"selectFolder" -> selectImageDir.launch()
} }
popupAction.dismiss() popupAction.dismiss()
} }
@@ -2,8 +2,6 @@ package io.legato.kazusa.ui.book.read
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legato.kazusa.R import io.legato.kazusa.R
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
@@ -32,9 +30,7 @@ import io.legato.kazusa.model.webBook.WebBook
import io.legato.kazusa.service.BaseReadAloudService import io.legato.kazusa.service.BaseReadAloudService
import io.legato.kazusa.ui.book.read.page.entities.TextChapter import io.legato.kazusa.ui.book.read.page.entities.TextChapter
import io.legato.kazusa.ui.book.searchContent.SearchResult import io.legato.kazusa.ui.book.searchContent.SearchResult
import io.legato.kazusa.utils.DocumentUtils import io.legato.kazusa.utils.ImageSaveUtils
import io.legato.kazusa.utils.FileUtils
import io.legato.kazusa.utils.isContentScheme
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.toStringArray import io.legato.kazusa.utils.toStringArray
@@ -48,10 +44,7 @@ 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 java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
/** /**
@@ -504,30 +497,23 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
/** /**
* 保存图片 * 保存图片
*/ */
fun saveImage(src: String?, uri: Uri) { fun saveImage(src: String?) {
src ?: return src ?: return
val book = ReadBook.book ?: return val book = ReadBook.book ?: return
execute { execute {
val image = BookHelp.getImage(book, src) val image = BookHelp.getImage(book, src)
FileInputStream(image).use { input -> val byteArray = image.readBytes()
if (uri.isContentScheme()) { val success = ImageSaveUtils.saveImageToGallery(
DocumentFile.fromTreeUri(context, uri)?.let { doc -> context,
val imageDoc = DocumentUtils.createFileIfNotExist(doc, image.name)!! byteArray,
context.contentResolver.openOutputStream(imageDoc.uri)!!.use { output -> folderName = "Legado"
input.copyTo(output) )
} if (!success) throw NoStackTraceException("保存到相册失败")
}
} else {
val dir = File(uri.path ?: uri.toString())
val file = FileUtils.createFileIfNotExist(dir, image.name)
FileOutputStream(file).use { output ->
input.copyTo(output)
}
}
}
}.onError { }.onError {
AppLog.put("保存图片出错\n${it.localizedMessage}", it) context.toastOnUi("保存图片失败: ${it.localizedMessage}")
context.toastOnUi("保存图片出错\n${it.localizedMessage}") }.onSuccess {
context.toastOnUi("已保存到相册")
} }
} }
@@ -28,11 +28,10 @@ import io.legato.kazusa.help.http.CookieStore
import io.legato.kazusa.help.source.SourceVerificationHelp import io.legato.kazusa.help.source.SourceVerificationHelp
import io.legato.kazusa.lib.dialogs.SelectItem 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.legado.app.lib.theme.accentColor //import io.legado.app.lib.theme.accentColor
import io.legato.kazusa.model.Download import io.legato.kazusa.model.Download
import io.legato.kazusa.ui.association.OnLineImportActivity import io.legato.kazusa.ui.association.OnLineImportActivity
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.gone import io.legato.kazusa.utils.gone
import io.legato.kazusa.utils.invisible import io.legato.kazusa.utils.invisible
import io.legato.kazusa.utils.keepScreenOn import io.legato.kazusa.utils.keepScreenOn
@@ -51,17 +50,10 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
override val binding by viewBinding(ActivityWebViewBinding::inflate) override val binding by viewBinding(ActivityWebViewBinding::inflate)
override val viewModel by viewModels<WebViewModel>() override val viewModel by viewModels<WebViewModel>()
private val imagePathKey = "imagePath"
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private var webPic: String? = null private var webPic: String? = null
private var isCloudflareChallenge = false private var isCloudflareChallenge = false
private var isFullScreen = false private var isFullScreen = false
private val saveImage = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
ACache.get().put(imagePathKey, uri.toString())
viewModel.saveImage(webPic, uri.toString())
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -183,9 +175,17 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) { ) {
hitTestResult.extra?.let { hitTestResult.extra?.let { webPic ->
saveImage(it) selector(
return@setOnLongClickListener true arrayListOf(
SelectItem(getString(R.string.action_save), "save"),
)
) { _, charSequence, _ ->
when (charSequence.value) {
"save" -> saveImage(webPic)
}
}
return@setOnLongClickListener false
} }
} }
return@setOnLongClickListener false return@setOnLongClickListener false
@@ -201,23 +201,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
private fun saveImage(webPic: String) { private fun saveImage(webPic: String) {
this.webPic = webPic this.webPic = webPic
val path = ACache.get().getAsString(imagePathKey) viewModel.saveImage(webPic)
if (path.isNullOrEmpty()) {
selectSaveFolder()
} else {
viewModel.saveImage(webPic, path)
}
}
private fun selectSaveFolder() {
val default = arrayListOf<SelectItem<Int>>()
val path = ACache.get().getAsString(imagePathKey)
if (!path.isNullOrEmpty()) {
default.add(SelectItem(path, -1))
}
saveImage.launch {
otherActions = default
}
} }
override fun finish() { override fun finish() {
@@ -2,13 +2,11 @@ package io.legato.kazusa.ui.browser
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.webkit.URLUtil import android.webkit.URLUtil
import android.webkit.WebView import android.webkit.WebView
import androidx.documentfile.provider.DocumentFile
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppConst import io.legato.kazusa.constant.AppConst.imagePathKey
import io.legato.kazusa.constant.SourceType import io.legato.kazusa.constant.SourceType
import io.legato.kazusa.data.appDb import io.legato.kazusa.data.appDb
import io.legato.kazusa.exception.NoStackTraceException import io.legato.kazusa.exception.NoStackTraceException
@@ -17,15 +15,11 @@ import io.legato.kazusa.help.http.okHttpClient
import io.legato.kazusa.help.source.SourceHelp import io.legato.kazusa.help.source.SourceHelp
import io.legato.kazusa.help.source.SourceVerificationHelp import io.legato.kazusa.help.source.SourceVerificationHelp
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
import io.legato.kazusa.utils.DocumentUtils import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.FileUtils import io.legato.kazusa.utils.ImageSaveUtils
import io.legato.kazusa.utils.isContentScheme
import io.legato.kazusa.utils.printOnDebug import io.legato.kazusa.utils.printOnDebug
import io.legato.kazusa.utils.toastOnUi import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.writeBytes
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
import java.io.File
import java.util.Date
class WebViewModel(application: Application) : BaseViewModel(application) { class WebViewModel(application: Application) : BaseViewModel(application) {
var intent: Intent? = null var intent: Intent? = null
@@ -66,26 +60,23 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun saveImage(webPic: String?, path: String) { fun saveImage(webPic: String?) {
webPic ?: return webPic ?: return
execute { execute {
val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg" val byteArray = webData2bitmap(webPic) ?: throw Throwable("NULL")
webData2bitmap(webPic)?.let { biteArray ->
if (path.isContentScheme()) { val success = ImageSaveUtils.saveImageToGallery(
val uri = Uri.parse(path) context,
DocumentFile.fromTreeUri(context, uri)?.let { doc -> byteArray,
DocumentUtils.createFileIfNotExist(doc, fileName) folderName = "Legado"
?.writeBytes(context, biteArray) )
}
} else { if (!success) throw Throwable("保存到相册失败")
val file = FileUtils.createFileIfNotExist(File(path), fileName)
file.writeBytes(biteArray)
}
} ?: throw Throwable("NULL")
}.onError { }.onError {
context.toastOnUi("保存图片失败:${it.localizedMessage}") ACache.get().remove(imagePathKey)
context.toastOnUi("保存图片失败: ${it.localizedMessage}")
}.onSuccess { }.onSuccess {
context.toastOnUi("保存成功") context.toastOnUi("已保存到相册")
} }
} }
@@ -30,7 +30,6 @@ import com.script.rhino.runScriptWithContext
import io.legato.kazusa.R import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.constant.AppConst import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppConst.imagePathKey
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.entities.RssSource import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.databinding.ActivityRssReadBinding import io.legato.kazusa.databinding.ActivityRssReadBinding
@@ -40,10 +39,8 @@ import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.lib.dialogs.selector import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.model.Download import io.legato.kazusa.model.Download
import io.legato.kazusa.ui.association.OnLineImportActivity import io.legato.kazusa.ui.association.OnLineImportActivity
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.login.SourceLoginActivity import io.legato.kazusa.ui.login.SourceLoginActivity
import io.legato.kazusa.ui.rss.favorites.RssFavoritesDialog import io.legato.kazusa.ui.rss.favorites.RssFavoritesDialog
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.NetworkUtils import io.legato.kazusa.utils.NetworkUtils
import io.legato.kazusa.utils.gone import io.legato.kazusa.utils.gone
import io.legato.kazusa.utils.invisible import io.legato.kazusa.utils.invisible
@@ -83,12 +80,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
private var ttsMenuItem: MenuItem? = null private var ttsMenuItem: MenuItem? = null
private var isFullScreen = false private var isFullScreen = false
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private val selectImageDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
ACache.get().put(imagePathKey, uri.toString())
viewModel.saveImage(it.value, uri)
}
}
private val rssJsExtensions by lazy { RssJsExtensions(this) } private val rssJsExtensions by lazy { RssJsExtensions(this) }
fun getSource(): RssSource? { fun getSource(): RssSource? {
@@ -257,12 +248,10 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
selector( selector(
arrayListOf( arrayListOf(
SelectItem(getString(R.string.action_save), "save"), SelectItem(getString(R.string.action_save), "save"),
SelectItem(getString(R.string.select_folder), "selectFolder")
) )
) { _, charSequence, _ -> ) { _, charSequence, _ ->
when (charSequence.value) { when (charSequence.value) {
"save" -> saveImage(webPic) "save" -> saveImage(webPic)
"selectFolder" -> selectSaveFolder(null)
} }
} }
return@setOnLongClickListener true return@setOnLongClickListener true
@@ -281,24 +270,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
} }
private fun saveImage(webPic: String) { private fun saveImage(webPic: String) {
val path = ACache.get().getAsString(imagePathKey) viewModel.saveImage(webPic)
if (path.isNullOrEmpty()) {
selectSaveFolder(webPic)
} else {
viewModel.saveImage(webPic, path.toUri())
}
}
private fun selectSaveFolder(webPic: String?) {
val default = arrayListOf<SelectItem<Int>>()
val path = ACache.get().getAsString(imagePathKey)
if (!path.isNullOrEmpty()) {
default.add(SelectItem(path, -1))
}
selectImageDir.launch {
otherActions = default
value = webPic
}
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@@ -2,14 +2,12 @@ package io.legato.kazusa.ui.rss.read
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.script.rhino.runScriptWithContext import com.script.rhino.runScriptWithContext
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.data.appDb import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.RssArticle import io.legato.kazusa.data.entities.RssArticle
import io.legato.kazusa.data.entities.RssSource import io.legato.kazusa.data.entities.RssSource
@@ -20,11 +18,10 @@ import io.legato.kazusa.help.http.newCallResponseBody
import io.legato.kazusa.help.http.okHttpClient import io.legato.kazusa.help.http.okHttpClient
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
import io.legato.kazusa.model.rss.Rss import io.legato.kazusa.model.rss.Rss
import io.legato.kazusa.utils.ImageSaveUtils
import io.legato.kazusa.utils.toastOnUi import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.writeBytes
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import splitties.init.appCtx import splitties.init.appCtx
import java.util.Date
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
@@ -170,19 +167,25 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun saveImage(webPic: String?, uri: Uri) { fun saveImage(webPic: String?) {
webPic ?: return webPic ?: return
execute { execute {
val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("NULL") val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("NULL")
uri.writeBytes(context, fileName, byteArray)
val success = ImageSaveUtils.saveImageToGallery(
context,
byteArray,
folderName = "Legado" // 可以自定义相册子目录
)
if (!success) throw NoStackTraceException("保存到相册失败")
}.onError { }.onError {
context.toastOnUi("保存图片失败:${it.localizedMessage}") context.toastOnUi("保存图片失败: ${it.localizedMessage}")
}.onSuccess { }.onSuccess {
context.toastOnUi("保存成功") context.toastOnUi("已保存到相册")
} }
} }
private suspend fun webData2bitmap(data: String): ByteArray? { private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) { return if (URLUtil.isValidUrl(data)) {
okHttpClient.newCallResponseBody { okHttpClient.newCallResponseBody {
@@ -0,0 +1,58 @@
package io.legato.kazusa.utils
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.provider.MediaStore
import io.legato.kazusa.constant.AppConst
import java.util.Date
object ImageSaveUtils {
/**
* 使用 MediaStore 保存图片到系统相册
* @param context Context
* @param byteArray 图片数据(JPG/PNG 的 byte[])
* @param prefix 文件名前缀,默认 "IMG_"
* @param folderName 相册下的子文件夹,默认 "Legado"
*/
fun saveImageToGallery(
context: Context,
byteArray: ByteArray,
prefix: String = "IMG_",
folderName: String = "Legado"
): Boolean {
return try {
val fileName = prefix + AppConst.fileNameFormat.format(Date()) + ".jpg"
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android10以上指定相对路径
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/$folderName")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
?: return false
resolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(byteArray)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
}
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}