添加删除文件操作,优化收藏文件操作,优化viewmodel数据共享。
This commit is contained in:
parent
c5c9785a96
commit
82270adb07
@ -0,0 +1,17 @@
|
|||||||
|
package com.all.pdfreader.pro.app.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件删除结果封装类
|
||||||
|
*/
|
||||||
|
data class DeleteResult(
|
||||||
|
val success: Boolean,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val deletedFiles: Int = 0,
|
||||||
|
val deletedSize: Long = 0
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun success(deletedFiles: Int = 1, deletedSize: Long = 0) =
|
||||||
|
DeleteResult(true, null, deletedFiles, deletedSize)
|
||||||
|
fun failure(message: String) = DeleteResult(false, message, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,4 +2,6 @@ package com.all.pdfreader.pro.app.model
|
|||||||
|
|
||||||
sealed class FileActionEvent {
|
sealed class FileActionEvent {
|
||||||
data class Rename(val renameResult: RenameResult) : FileActionEvent()
|
data class Rename(val renameResult: RenameResult) : FileActionEvent()
|
||||||
|
data class Delete(val deleteResult: DeleteResult) : FileActionEvent()
|
||||||
|
data class Favorite(val isFavorite: Boolean) : FileActionEvent()
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
updateSelectedNav(activeFragment)
|
updateSelectedNav(activeFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initObserve(){
|
private fun initObserve() {
|
||||||
//观察其余操作
|
//观察其余操作
|
||||||
viewModel.fileActionEvent.observe(this) { event ->
|
viewModel.fileActionEvent.observe(this) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
@ -77,6 +77,22 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
showToast(event.renameResult.errorMessage.toString())
|
showToast(event.renameResult.errorMessage.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is FileActionEvent.Delete -> {
|
||||||
|
if (event.deleteResult.success) {
|
||||||
|
showToast(getString(R.string.delete_successfully))
|
||||||
|
} else {
|
||||||
|
showToast(event.deleteResult.errorMessage.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FileActionEvent.Favorite -> {
|
||||||
|
if (event.isFavorite) {
|
||||||
|
showToast(getString(R.string.added_to_favorites))
|
||||||
|
} else {
|
||||||
|
showToast(getString(R.string.removed_from_favorites))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,19 +59,6 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//观察其余操作
|
|
||||||
viewModel.fileActionEvent.observe(this) { event ->
|
|
||||||
logDebug("fileActionEvent响应 $event")
|
|
||||||
when (event) {
|
|
||||||
is FileActionEvent.Rename -> {
|
|
||||||
if (event.renameResult.success) {
|
|
||||||
showToast(getString(R.string.rename_successfully))
|
|
||||||
} else {
|
|
||||||
showToast(event.renameResult.errorMessage.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPdf() {
|
private fun loadPdf() {
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.all.pdfreader.pro.app.ui.dialog
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.all.pdfreader.pro.app.R
|
||||||
|
import com.all.pdfreader.pro.app.databinding.DialogDeleteBinding
|
||||||
|
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||||
|
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
||||||
|
|
||||||
|
class DeleteDialogFragment() : DialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: DialogDeleteBinding
|
||||||
|
private val viewModel: PdfViewModel by activityViewModels()
|
||||||
|
private lateinit var pdfDocument: PdfDocumentEntity
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = DialogDeleteBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
dialog?.window?.apply {
|
||||||
|
// 去掉系统默认的背景 padding
|
||||||
|
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||||
|
// 设置宽度为全屏减去 16dp
|
||||||
|
val margin = resources.getDimensionPixelSize(R.dimen.dialog_margin) // 16dp
|
||||||
|
val width = resources.displayMetrics.widthPixels - margin * 2
|
||||||
|
setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewModel.pdfDocument.value?.let {
|
||||||
|
pdfDocument = it
|
||||||
|
setupOnClick()
|
||||||
|
} ?: run {
|
||||||
|
showToast(getString(R.string.file_not))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupOnClick() {
|
||||||
|
binding.okBtn.setOnClickListener {
|
||||||
|
viewModel.deleteFile(pdfDocument.filePath)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.cancelBtn.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(message: String) {
|
||||||
|
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,6 @@ import java.io.File
|
|||||||
class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment() {
|
class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
private lateinit var binding: DialogListMoreBinding
|
private lateinit var binding: DialogListMoreBinding
|
||||||
private val pdfRepository = PdfRepository.getInstance()
|
|
||||||
private val viewModel: PdfViewModel by activityViewModels()
|
private val viewModel: PdfViewModel by activityViewModels()
|
||||||
private lateinit var pdfDocument: PdfDocumentEntity
|
private lateinit var pdfDocument: PdfDocumentEntity
|
||||||
private var isFavorite: Boolean = false
|
private var isFavorite: Boolean = false
|
||||||
@ -54,7 +53,6 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
|
|||||||
isFavorite = pdfDocument.isFavorite
|
isFavorite = pdfDocument.isFavorite
|
||||||
initUi()
|
initUi()
|
||||||
setupOnClick()
|
setupOnClick()
|
||||||
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
showToast(getString(R.string.file_not))
|
showToast(getString(R.string.file_not))
|
||||||
dismiss()
|
dismiss()
|
||||||
@ -84,28 +82,16 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
|
|||||||
binding.collectBtn.setClickWithAnimation(duration = 250) {
|
binding.collectBtn.setClickWithAnimation(duration = 250) {
|
||||||
isFavorite = !isFavorite
|
isFavorite = !isFavorite
|
||||||
updateCollectUi(isFavorite)
|
updateCollectUi(isFavorite)
|
||||||
saveCollectState(isFavorite)
|
viewModel.saveCollectState(pdfDocument.filePath, isFavorite)
|
||||||
}
|
|
||||||
binding.renameFileBtn.setOnClickListener {
|
|
||||||
RenameDialogFragment(pdfDocument.filePath).show(
|
|
||||||
parentFragmentManager,
|
|
||||||
"ListMoreDialogFragment"
|
|
||||||
)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
binding.renameFileBtn.setOnClickListener {
|
||||||
|
RenameDialogFragment().show(parentFragmentManager, "ListMoreDialogFragment")
|
||||||
private fun saveCollectState(b: Boolean) {
|
dismiss()
|
||||||
pdfDocument = pdfDocument.copy(
|
|
||||||
isFavorite = b
|
|
||||||
)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
pdfRepository.updateFavoriteStatus(pdfDocument.filePath, pdfDocument.isFavorite)
|
|
||||||
}
|
}
|
||||||
if (b) {
|
binding.deleteFileBtn.setOnClickListener {
|
||||||
showToast(getString(R.string.added_to_favorites))
|
DeleteDialogFragment().show(parentFragmentManager, "DeleteDialogFragment")
|
||||||
} else {
|
dismiss()
|
||||||
showToast(getString(R.string.removed_from_favorites))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,7 @@ import com.all.pdfreader.pro.app.util.FileUtils
|
|||||||
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class RenameDialogFragment(
|
class RenameDialogFragment() : DialogFragment() {
|
||||||
private val filePath: String
|
|
||||||
) : DialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: DialogRenameFileBinding
|
private lateinit var binding: DialogRenameFileBinding
|
||||||
private val viewModel: PdfViewModel by activityViewModels()
|
private val viewModel: PdfViewModel by activityViewModels()
|
||||||
@ -49,18 +47,14 @@ class RenameDialogFragment(
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewModel.pdfDocument.value?.let {
|
||||||
viewModel.pdfDocument.observe(this) { document ->
|
pdfDocument = it
|
||||||
document?.let {
|
initView()
|
||||||
pdfDocument = it
|
setupOnClick()
|
||||||
initView()
|
} ?: run {
|
||||||
setupOnClick()
|
showToast(getString(R.string.file_not))
|
||||||
} ?: run {
|
dismiss()
|
||||||
showToast(getString(R.string.file_not))
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewModel.getPDFDocument(filePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
|||||||
@ -0,0 +1,264 @@
|
|||||||
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import com.all.pdfreader.pro.app.PRApp
|
||||||
|
import com.all.pdfreader.pro.app.R
|
||||||
|
import com.all.pdfreader.pro.app.model.DeleteResult
|
||||||
|
import com.all.pdfreader.pro.app.util.FileUtils.formatFileSize
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
|
||||||
|
object FileDeleteUtil {
|
||||||
|
/**
|
||||||
|
* 删除文件(同步版本)- 功能完善的删除方法
|
||||||
|
*
|
||||||
|
* @param file 要删除的文件
|
||||||
|
* @param deleteEmptyParentDirs 是否删除空的父目录(默认false)
|
||||||
|
* @param dryRun 是否只模拟删除不实际执行(默认false)
|
||||||
|
* @return DeleteResult对象,包含删除结果和详细信息
|
||||||
|
*/
|
||||||
|
fun deleteFile(
|
||||||
|
file: File,
|
||||||
|
deleteEmptyParentDirs: Boolean = false,
|
||||||
|
dryRun: Boolean = false
|
||||||
|
): DeleteResult {
|
||||||
|
if (!file.exists()) {
|
||||||
|
Log.e("ocean", "❌ File does not exist: ${file.path}")
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_file_not_exist))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.canWrite()) {
|
||||||
|
Log.e("ocean", "❌ No write permission for file: ${file.path}")
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_no_write_permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全检查:防止删除重要系统目录
|
||||||
|
if (isProtectedPath(file)) {
|
||||||
|
Log.e("ocean", "❌ Cannot delete protected path: ${file.path}")
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_cannot_delete_protected_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是目录,检查是否为空(避免误删非空目录)
|
||||||
|
if (file.isDirectory) {
|
||||||
|
val files = file.listFiles()
|
||||||
|
if (!files.isNullOrEmpty()) {
|
||||||
|
Log.e("ocean", "❌ Directory is not empty: ${file.path} (${files.size} items)")
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_directory_not_empty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算要删除的文件大小
|
||||||
|
val fileSize = file.length()
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
Log.d("ocean", "🔍 Dry run - would delete: ${file.path} (${formatFileSize(fileSize)})")
|
||||||
|
return DeleteResult.success(1, fileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val success = file.delete()
|
||||||
|
if (success) {
|
||||||
|
Log.d(
|
||||||
|
"ocean",
|
||||||
|
"✅ File deleted successfully: ${file.path} (${formatFileSize(fileSize)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果需要,删除空的父目录
|
||||||
|
if (deleteEmptyParentDirs) {
|
||||||
|
deleteEmptyParentDirectories(file.parentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteResult.success(1, fileSize)
|
||||||
|
} else {
|
||||||
|
Log.e("ocean", "❌ Failed to delete file: ${file.path}")
|
||||||
|
DeleteResult.failure(PRApp.getStringRes(R.string.error_failed_delete_file))
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e("ocean", "❌ Security exception while deleting: ${e.message}")
|
||||||
|
DeleteResult.failure(PRApp.getStringRes(R.string.error_permission_delete_file))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ocean", "❌ Exception while deleting file: ${e.message}")
|
||||||
|
DeleteResult.failure(
|
||||||
|
PRApp.getStringRes(
|
||||||
|
R.string.error_file_delete_failed,
|
||||||
|
e.message.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件(异步版本)- 功能完善的删除方法
|
||||||
|
*
|
||||||
|
* @param file 要删除的文件
|
||||||
|
* @param deleteEmptyParentDirs 是否删除空的父目录(默认false)
|
||||||
|
* @param dryRun 是否只模拟删除不实际执行(默认false)
|
||||||
|
* @return DeleteResult对象,包含删除结果和详细信息
|
||||||
|
*/
|
||||||
|
suspend fun deleteFileAsync(
|
||||||
|
file: File,
|
||||||
|
deleteEmptyParentDirs: Boolean = false,
|
||||||
|
dryRun: Boolean = false
|
||||||
|
): DeleteResult =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
deleteFile(file, deleteEmptyParentDirs, dryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文件
|
||||||
|
*
|
||||||
|
* @param files 要删除的文件列表
|
||||||
|
* @param deleteEmptyParentDirs 是否删除空的父目录(默认false)
|
||||||
|
* @param dryRun 是否只模拟删除不实际执行(默认false)
|
||||||
|
* @return DeleteResult对象,包含总体删除结果
|
||||||
|
*/
|
||||||
|
fun deleteFiles(
|
||||||
|
files: List<File>,
|
||||||
|
deleteEmptyParentDirs: Boolean = false,
|
||||||
|
dryRun: Boolean = false
|
||||||
|
): DeleteResult {
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
return DeleteResult.success(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalDeleted = 0
|
||||||
|
var totalSize = 0L
|
||||||
|
val errors = mutableListOf<String>()
|
||||||
|
|
||||||
|
files.forEach { file ->
|
||||||
|
val result = deleteFile(file, deleteEmptyParentDirs, dryRun)
|
||||||
|
if (result.success) {
|
||||||
|
totalDeleted += result.deletedFiles
|
||||||
|
totalSize += result.deletedSize
|
||||||
|
} else {
|
||||||
|
errors.add("${file.name}: ${result.errorMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (errors.isEmpty()) {
|
||||||
|
DeleteResult.success(totalDeleted, totalSize)
|
||||||
|
} else {
|
||||||
|
val error = "\n${errors.joinToString("\n")}"
|
||||||
|
DeleteResult.failure(PRApp.getStringRes(R.string.error_some_files_not_deleted, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除目录及其所有内容(递归删除)
|
||||||
|
*
|
||||||
|
* @param directory 要删除的目录
|
||||||
|
* @param dryRun 是否只模拟删除不实际执行(默认false)
|
||||||
|
* @return DeleteResult对象,包含删除结果
|
||||||
|
*/
|
||||||
|
fun deleteDirectoryRecursive(directory: File, dryRun: Boolean = false): DeleteResult {
|
||||||
|
if (!directory.exists()) {
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_directory_does_not_exist))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directory.isDirectory) {
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_path_not_directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProtectedPath(directory)) {
|
||||||
|
return DeleteResult.failure(PRApp.getStringRes(R.string.error_cannot_delete_protected_directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
var deletedCount = 0
|
||||||
|
var deletedSize = 0L
|
||||||
|
|
||||||
|
// 先删除目录下的所有文件和子目录
|
||||||
|
directory.listFiles()?.forEach { file ->
|
||||||
|
val result = if (file.isDirectory) {
|
||||||
|
deleteDirectoryRecursive(file, dryRun)
|
||||||
|
} else {
|
||||||
|
deleteFile(file, false, dryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
deletedCount += result.deletedFiles
|
||||||
|
deletedSize += result.deletedSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后删除空目录本身
|
||||||
|
val dirResult = deleteFile(directory, false, dryRun)
|
||||||
|
if (dirResult.success) {
|
||||||
|
deletedCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeleteResult.success(deletedCount, deletedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为受保护的路径(防止误删系统文件)
|
||||||
|
*/
|
||||||
|
private fun isProtectedPath(file: File): Boolean {
|
||||||
|
val protectedPaths = listOf(
|
||||||
|
"/system",
|
||||||
|
"/data",
|
||||||
|
"/vendor",
|
||||||
|
"/sbin",
|
||||||
|
"/etc",
|
||||||
|
"/dev",
|
||||||
|
"/proc",
|
||||||
|
"/sys",
|
||||||
|
"/root",
|
||||||
|
"/acct",
|
||||||
|
"/mnt",
|
||||||
|
"/storage/emulated/0/Android",
|
||||||
|
"/storage/emulated/0/.android_secure"
|
||||||
|
)
|
||||||
|
|
||||||
|
val absolutePath = file.absolutePath
|
||||||
|
return protectedPaths.any { protectedPath ->
|
||||||
|
absolutePath.startsWith(protectedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除空的父目录(向上递归)
|
||||||
|
*/
|
||||||
|
private fun deleteEmptyParentDirectories(directory: File?) {
|
||||||
|
directory?.let { dir ->
|
||||||
|
if (dir.exists() && dir.isDirectory && dir.listFiles()?.isEmpty() == true) {
|
||||||
|
val parent = dir.parentFile
|
||||||
|
if (dir.delete()) {
|
||||||
|
Log.d("ocean", "✅ Deleted empty parent directory: ${dir.path}")
|
||||||
|
deleteEmptyParentDirectories(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全删除文件到回收站(如果系统支持)
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @param file 要删除的文件
|
||||||
|
* @return DeleteResult对象
|
||||||
|
*/
|
||||||
|
fun moveToTrash(context: Context, file: File): DeleteResult {
|
||||||
|
return try {
|
||||||
|
// Android 10+ 支持系统回收站
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
MediaStore.createTrashRequest(
|
||||||
|
context.contentResolver,
|
||||||
|
listOf(Uri.fromFile(file)),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
// 注意:这里需要Activity来启动IntentSender
|
||||||
|
DeleteResult.success(1, file.length())
|
||||||
|
} else {
|
||||||
|
// 低版本直接删除
|
||||||
|
deleteFile(file)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DeleteResult.failure("Failed to move file to trash: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import android.graphics.pdf.PdfRenderer
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.all.pdfreader.pro.app.PRApp
|
import com.all.pdfreader.pro.app.PRApp
|
||||||
import com.all.pdfreader.pro.app.R
|
import com.all.pdfreader.pro.app.R
|
||||||
@ -35,7 +35,6 @@ object FileUtils {
|
|||||||
// 外部存储根目录(需要权限)
|
// 外部存储根目录(需要权限)
|
||||||
if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
||||||
Log.d("ocean", "📂 扫描外部存储目录...")
|
Log.d("ocean", "📂 扫描外部存储目录...")
|
||||||
|
|
||||||
// 扫描常见的PDF存储目录
|
// 扫描常见的PDF存储目录
|
||||||
val externalStorage = android.os.Environment.getExternalStorageDirectory()
|
val externalStorage = android.os.Environment.getExternalStorageDirectory()
|
||||||
scanCommonDirectories(externalStorage, pdfFiles)
|
scanCommonDirectories(externalStorage, pdfFiles)
|
||||||
@ -431,7 +430,11 @@ object FileUtils {
|
|||||||
RenameResult.failure(PRApp.getStringRes(R.string.error_insufficient_permission))
|
RenameResult.failure(PRApp.getStringRes(R.string.error_insufficient_permission))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ocean", "❌ File rename exception: ${e.message}")
|
Log.e("ocean", "❌ File rename exception: ${e.message}")
|
||||||
RenameResult.failure(PRApp.getStringRes(R.string.error_file_rename_exception, e.message.toString()))
|
RenameResult.failure(
|
||||||
|
PRApp.getStringRes(
|
||||||
|
R.string.error_file_rename_exception, e.message.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,7 +519,7 @@ object FileUtils {
|
|||||||
}
|
}
|
||||||
tempFile
|
tempFile
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ocean", "无法获取文件: ${e.message}", e)
|
// Log.e("ocean", "无法获取文件: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,12 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.all.pdfreader.pro.app.model.FileActionEvent
|
import com.all.pdfreader.pro.app.model.FileActionEvent
|
||||||
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||||
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
||||||
|
import com.all.pdfreader.pro.app.util.FileDeleteUtil
|
||||||
import com.all.pdfreader.pro.app.util.FileUtils
|
import com.all.pdfreader.pro.app.util.FileUtils
|
||||||
import com.all.pdfreader.pro.app.util.LogUtil
|
import com.all.pdfreader.pro.app.util.LogUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class PdfViewModel : ViewModel() {
|
class PdfViewModel : ViewModel() {
|
||||||
@ -23,7 +26,6 @@ class PdfViewModel : ViewModel() {
|
|||||||
private val _fileActionEvent = MutableLiveData<FileActionEvent>()
|
private val _fileActionEvent = MutableLiveData<FileActionEvent>()
|
||||||
val fileActionEvent: LiveData<FileActionEvent> = _fileActionEvent
|
val fileActionEvent: LiveData<FileActionEvent> = _fileActionEvent
|
||||||
|
|
||||||
|
|
||||||
fun getPDFDocument(filePath: String) {
|
fun getPDFDocument(filePath: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val document = pdfRepository.getDocumentByPath(filePath)
|
val document = pdfRepository.getDocumentByPath(filePath)
|
||||||
@ -37,7 +39,10 @@ class PdfViewModel : ViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val oldFile = File(filePath)
|
val oldFile = File(filePath)
|
||||||
val renameResult = FileUtils.renameFile(oldFile, newName)
|
val renameResult = FileUtils.renameFile(oldFile, newName)
|
||||||
Log.d("ocean", "renamePdf->oldFile: $oldFile, newName=$newName, renameResult=$renameResult")
|
Log.d(
|
||||||
|
"ocean",
|
||||||
|
"renamePdf->oldFile: $oldFile, newName=$newName, renameResult=$renameResult"
|
||||||
|
)
|
||||||
if (renameResult.success) {//修改成功更新数据库
|
if (renameResult.success) {//修改成功更新数据库
|
||||||
val finalName = if (newName.contains('.')) {
|
val finalName = if (newName.contains('.')) {
|
||||||
// 用户提供了后缀,使用用户的
|
// 用户提供了后缀,使用用户的
|
||||||
@ -56,7 +61,34 @@ class PdfViewModel : ViewModel() {
|
|||||||
Log.d("ocean", "renamePdf->newFilePath: $newFilePath, finalName=$finalName")
|
Log.d("ocean", "renamePdf->newFilePath: $newFilePath, finalName=$finalName")
|
||||||
pdfRepository.updateFilePathAndFileName(filePath, newFilePath, finalName)
|
pdfRepository.updateFilePathAndFileName(filePath, newFilePath, finalName)
|
||||||
}
|
}
|
||||||
_fileActionEvent.postValue(FileActionEvent.Rename(renameResult))
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_fileActionEvent.postValue(FileActionEvent.Rename(renameResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteFile(filePath: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val file = File(filePath)
|
||||||
|
val deleteResult = FileDeleteUtil.deleteFile(file)
|
||||||
|
Log.d("ocean", "deleteFile->file: $file, deleteResult=$deleteResult")
|
||||||
|
if (deleteResult.success) {
|
||||||
|
Log.d("ocean", "文件已删除,清除数据库数据")
|
||||||
|
pdfRepository.deleteDocument(filePath)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_fileActionEvent.postValue(FileActionEvent.Delete(deleteResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCollectState(filePath: String, isFavorite: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
pdfRepository.updateFavoriteStatus(filePath, isFavorite)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_fileActionEvent.postValue(FileActionEvent.Favorite(isFavorite))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
app/src/main/res/drawable/dr_click_btn_red_bg.xml
Normal file
20
app/src/main/res/drawable/dr_click_btn_red_bg.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- 按下状态 -->
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#ED343B"/> <!-- 按下颜色:深一点 -->
|
||||||
|
<corners android:radius="12dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- 默认状态 -->
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#F1494F"/> <!-- 默认颜色 -->
|
||||||
|
<corners android:radius="12dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</selector>
|
||||||
72
app/src/main/res/layout/dialog_delete.xml
Normal file
72
app/src/main/res/layout/dialog_delete.xml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/dr_rounded_corner_12_bg_white"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:fontFamily="@font/poppins_semibold"
|
||||||
|
android:text="@string/delete_file_title"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:fontFamily="@font/poppins_regular"
|
||||||
|
android:text="@string/delete_file_desc"
|
||||||
|
android:textColor="@color/black_80"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/cancelBtn"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/dr_cancel_btn_bg"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/poppins_semibold"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
android:textColor="@color/black_60"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/okBtn"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/dr_click_btn_red_bg"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/poppins_semibold"
|
||||||
|
android:text="@string/delete"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -269,6 +269,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/deleteFileBtn"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
|||||||
@ -46,6 +46,7 @@
|
|||||||
<string name="added_to_favorites">Added to Favorite</string>
|
<string name="added_to_favorites">Added to Favorite</string>
|
||||||
<string name="removed_from_favorites">Removed from Favorites</string>
|
<string name="removed_from_favorites">Removed from Favorites</string>
|
||||||
<string name="delete_file">Delete File</string>
|
<string name="delete_file">Delete File</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
<string name="set_password">Set Password</string>
|
<string name="set_password">Set Password</string>
|
||||||
<string name="remove_password">Remove Password</string>
|
<string name="remove_password">Remove Password</string>
|
||||||
<string name="duplicate_file">Duplicate File</string>
|
<string name="duplicate_file">Duplicate File</string>
|
||||||
@ -57,6 +58,7 @@
|
|||||||
<string name="name_already_exists">A file with the same name already exists</string>
|
<string name="name_already_exists">A file with the same name already exists</string>
|
||||||
<string name="name_start_end_space">File name cannot start or end with space</string>
|
<string name="name_start_end_space">File name cannot start or end with space</string>
|
||||||
<string name="rename_successfully">Rename successfully</string>
|
<string name="rename_successfully">Rename successfully</string>
|
||||||
|
<string name="delete_successfully">Delete successfully</string>
|
||||||
<string name="error_file_not_exist">File does not exist</string>
|
<string name="error_file_not_exist">File does not exist</string>
|
||||||
<string name="error_no_write_permission">No write permission for file</string>
|
<string name="error_no_write_permission">No write permission for file</string>
|
||||||
<string name="error_invalid_file_name">Invalid file name</string>
|
<string name="error_invalid_file_name">Invalid file name</string>
|
||||||
@ -65,4 +67,15 @@
|
|||||||
<string name="error_file_rename_failed">File rename failed</string>
|
<string name="error_file_rename_failed">File rename failed</string>
|
||||||
<string name="error_insufficient_permission">Insufficient permission to rename file</string>
|
<string name="error_insufficient_permission">Insufficient permission to rename file</string>
|
||||||
<string name="error_file_rename_exception">File rename exception: %1$s</string>
|
<string name="error_file_rename_exception">File rename exception: %1$s</string>
|
||||||
|
<string name="error_cannot_delete_protected_path">Cannot delete protected system path</string>
|
||||||
|
<string name="error_directory_not_empty">Directory is not empty</string>
|
||||||
|
<string name="error_failed_delete_file">Failed to delete file</string>
|
||||||
|
<string name="error_permission_delete_file">Insufficient permission to delete file</string>
|
||||||
|
<string name="error_file_delete_failed">Delete operation failed: %1$s</string>
|
||||||
|
<string name="error_some_files_not_deleted">Some files could not be deleted: %1$s</string>
|
||||||
|
<string name="error_directory_does_not_exist">Directory does not exist</string>
|
||||||
|
<string name="error_path_not_directory">Path is not a directory</string>
|
||||||
|
<string name="error_cannot_delete_protected_directory">Cannot delete protected system directory</string>
|
||||||
|
<string name="delete_file_title">Delete this file permanently?</string>
|
||||||
|
<string name="delete_file_desc">Deleting this file will remove it permanently from your device.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in New Issue
Block a user