使用MediaStore保存图片
This commit is contained in:
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user