1.设置密码
2.复制文件 3.优化耗时操作进度条展示
This commit is contained in:
parent
5323ad1324
commit
aead309db7
@ -1,7 +1,15 @@
|
|||||||
package com.all.pdfreader.pro.app.model
|
package com.all.pdfreader.pro.app.model
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
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 Delete(val deleteResult: DeleteResult) : FileActionEvent()
|
||||||
data class Favorite(val isFavorite: Boolean) : FileActionEvent()
|
data class Favorite(val isFavorite: Boolean) : FileActionEvent()
|
||||||
|
data class Duplicate(val file: File?) : FileActionEvent()
|
||||||
|
|
||||||
|
// 成功或失败结果,只在完成事件时有值
|
||||||
|
data class SetPassword(val status: Status, val success: Boolean? = null) : FileActionEvent() {
|
||||||
|
enum class Status { START, COMPLETE }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,25 +6,26 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface PdfDocumentDao {
|
interface PdfDocumentDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertOrUpdate(document: PdfDocumentEntity)
|
suspend fun insertOrUpdate(document: PdfDocumentEntity)
|
||||||
|
|
||||||
//@Update 不会响应flow
|
//@Update 不会响应flow
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(document: PdfDocumentEntity)
|
suspend fun update(document: PdfDocumentEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM pdf_documents WHERE fileHash = :fileHash")
|
@Query("SELECT * FROM pdf_documents WHERE fileHash = :fileHash")
|
||||||
suspend fun getByHash(fileHash: String): PdfDocumentEntity?
|
suspend fun getByHash(fileHash: String): PdfDocumentEntity?
|
||||||
|
|
||||||
@Query("SELECT * FROM pdf_documents WHERE filePath = :filePath")
|
@Query("SELECT * FROM pdf_documents WHERE filePath = :filePath")
|
||||||
suspend fun getByPath(filePath: String): PdfDocumentEntity?
|
suspend fun getByPath(filePath: String): PdfDocumentEntity?
|
||||||
|
|
||||||
@Query("SELECT * FROM pdf_documents WHERE isFavorite = 1 ORDER BY addedToFavoriteTime DESC")
|
@Query("SELECT * FROM pdf_documents WHERE isFavorite = 1 ORDER BY addedToFavoriteTime DESC")
|
||||||
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>>
|
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM pdf_documents WHERE fileName LIKE '%' || :query || '%' OR metadataTitle LIKE '%' || :query || '%'")
|
@Query("SELECT * FROM pdf_documents WHERE fileName LIKE '%' || :query || '%' OR metadataTitle LIKE '%' || :query || '%'")
|
||||||
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>>
|
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
|
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
|
||||||
fun getAllDocuments(): Flow<List<PdfDocumentEntity>>
|
fun getAllDocuments(): Flow<List<PdfDocumentEntity>>
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ interface PdfDocumentDao {
|
|||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(document: PdfDocumentEntity)
|
suspend fun delete(document: PdfDocumentEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM pdf_documents WHERE filePath = :filePath")
|
@Query("DELETE FROM pdf_documents WHERE filePath = :filePath")
|
||||||
suspend fun deleteByPath(filePath: String)
|
suspend fun deleteByPath(filePath: String)
|
||||||
|
|
||||||
@ -41,4 +42,6 @@ interface PdfDocumentDao {
|
|||||||
@Query("UPDATE pdf_documents SET filePath = :newFilePath, fileName = :newName WHERE filePath = :oldFilePath")
|
@Query("UPDATE pdf_documents SET filePath = :newFilePath, fileName = :newName WHERE filePath = :oldFilePath")
|
||||||
suspend fun updateFilePathAndFileName(oldFilePath: String, newFilePath: String, newName: String)
|
suspend fun updateFilePathAndFileName(oldFilePath: String, newFilePath: String, newName: String)
|
||||||
|
|
||||||
|
@Query("UPDATE pdf_documents SET isPassword = :hasPassword WHERE filePath = :filePath")
|
||||||
|
suspend fun updateIsPassword(filePath: String, hasPassword: Boolean)
|
||||||
}
|
}
|
||||||
@ -43,6 +43,11 @@ class PdfRepository private constructor(context: Context) {
|
|||||||
pdfDao.updateFilePathAndFileName(oldFilePath, newFilePath, newName)
|
pdfDao.updateFilePathAndFileName(oldFilePath, newFilePath, newName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//语句更新可响应flow
|
||||||
|
suspend fun updateIsPassword(filePath: String, hasPassword: Boolean){
|
||||||
|
pdfDao.updateIsPassword(filePath, hasPassword)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun updateFavoriteStatus(filePath: String, isFavorite: Boolean) {
|
suspend fun updateFavoriteStatus(filePath: String, isFavorite: Boolean) {
|
||||||
val document = pdfDao.getByPath(filePath)?.copy(
|
val document = pdfDao.getByPath(filePath)?.copy(
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
@ -60,6 +65,7 @@ class PdfRepository private constructor(context: Context) {
|
|||||||
document?.let { pdfDao.update(it) }
|
document?.let { pdfDao.update(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//复制更新不会更新flow
|
||||||
suspend fun updatePasswordStatus(filePath: String, isPassword: Boolean) {
|
suspend fun updatePasswordStatus(filePath: String, isPassword: Boolean) {
|
||||||
val document = pdfDao.getByPath(filePath)?.copy(
|
val document = pdfDao.getByPath(filePath)?.copy(
|
||||||
isPassword = isPassword
|
isPassword = isPassword
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import com.all.pdfreader.pro.app.R
|
|||||||
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
|
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
|
||||||
import com.all.pdfreader.pro.app.model.FileActionEvent
|
import com.all.pdfreader.pro.app.model.FileActionEvent
|
||||||
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
|
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
|
||||||
|
import com.all.pdfreader.pro.app.ui.dialog.ProgressDialogFragment
|
||||||
import com.all.pdfreader.pro.app.ui.dialog.SortDialogFragment
|
import com.all.pdfreader.pro.app.ui.dialog.SortDialogFragment
|
||||||
import com.all.pdfreader.pro.app.ui.fragment.FavoriteFrag
|
import com.all.pdfreader.pro.app.ui.fragment.FavoriteFrag
|
||||||
import com.all.pdfreader.pro.app.ui.fragment.HomeFrag
|
import com.all.pdfreader.pro.app.ui.fragment.HomeFrag
|
||||||
@ -43,6 +44,8 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
|
|
||||||
private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] }
|
private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] }
|
||||||
|
|
||||||
|
private var progressDialog: ProgressDialogFragment? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
@ -93,6 +96,33 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
showToast(getString(R.string.removed_from_favorites))
|
showToast(getString(R.string.removed_from_favorites))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is FileActionEvent.Duplicate -> {
|
||||||
|
if (event.file != null) {
|
||||||
|
showToast(getString(R.string.duplicate_created_successfully))
|
||||||
|
} else {
|
||||||
|
showToast(getString(R.string.duplicate_created_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FileActionEvent.SetPassword -> {
|
||||||
|
when (event.status) {
|
||||||
|
FileActionEvent.SetPassword.Status.START -> {
|
||||||
|
progressDialog = ProgressDialogFragment()
|
||||||
|
progressDialog?.show(supportFragmentManager, "progressDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
FileActionEvent.SetPassword.Status.COMPLETE -> {
|
||||||
|
progressDialog?.dismiss()
|
||||||
|
progressDialog = null
|
||||||
|
if (event.success == true) {
|
||||||
|
showToast(getString(R.string.set_password_successfully))
|
||||||
|
} else {
|
||||||
|
showToast(getString(R.string.set_password_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
|
|||||||
.into(binding.tvFileImg)
|
.into(binding.tvFileImg)
|
||||||
}
|
}
|
||||||
updateCollectUi(isFavorite)
|
updateCollectUi(isFavorite)
|
||||||
|
updatePasswordUi(pdfDocument.isPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupOnClick() {
|
private fun setupOnClick() {
|
||||||
@ -103,6 +104,18 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
|
|||||||
printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath)))
|
printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath)))
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
binding.duplicateFileBtn.setOnClickListener {
|
||||||
|
viewModel.duplicateFile(requireActivity(), pdfDocument.filePath)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.setPasswordBtn.setOnClickListener {
|
||||||
|
if (pdfDocument.isPassword) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
PdfSetPasswordDialog().show(parentFragmentManager, "PdfSetPasswordDialog")
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCollectUi(b: Boolean) {
|
private fun updateCollectUi(b: Boolean) {
|
||||||
@ -113,6 +126,16 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updatePasswordUi(b: Boolean) {
|
||||||
|
if (b) {
|
||||||
|
binding.passwordIv.setImageResource(R.drawable.unlock)
|
||||||
|
binding.passwordTv.text = getString(R.string.remove_password)
|
||||||
|
} else {
|
||||||
|
binding.passwordIv.setImageResource(R.drawable.lock)
|
||||||
|
binding.passwordTv.text = getString(R.string.set_password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showToast(message: String) {
|
private fun showToast(message: String) {
|
||||||
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,18 +7,23 @@ import android.text.TextWatcher
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.all.pdfreader.pro.app.R
|
import com.all.pdfreader.pro.app.R
|
||||||
import com.all.pdfreader.pro.app.databinding.DialogPdfSetPasswordBinding
|
import com.all.pdfreader.pro.app.databinding.DialogPdfSetPasswordBinding
|
||||||
|
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||||
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
|
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
|
||||||
|
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
||||||
|
import kotlin.getValue
|
||||||
|
|
||||||
class PdfSetPasswordDialog(
|
class PdfSetPasswordDialog() : DialogFragment(
|
||||||
private val onCancelled: () -> Unit, private val onPasswordSet: (String) -> Unit
|
|
||||||
) : DialogFragment(
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
private lateinit var binding: DialogPdfSetPasswordBinding
|
private lateinit var binding: DialogPdfSetPasswordBinding
|
||||||
|
private val viewModel: PdfViewModel by activityViewModels()
|
||||||
|
private lateinit var pdfDocument: PdfDocumentEntity
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
@ -42,14 +47,19 @@ class PdfSetPasswordDialog(
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.etPassword.showKeyboard()
|
viewModel.pdfDocument.value?.let {
|
||||||
setupListeners()
|
pdfDocument = it
|
||||||
setupTextWatchers()
|
binding.etPassword.showKeyboard()
|
||||||
|
setupListeners()
|
||||||
|
setupTextWatchers()
|
||||||
|
} ?: run {
|
||||||
|
showToast(getString(R.string.file_not))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
binding.tvCancel.setOnClickListener {
|
binding.tvCancel.setOnClickListener {
|
||||||
onCancelled()
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +67,7 @@ class PdfSetPasswordDialog(
|
|||||||
val password = binding.etPassword.text.toString()
|
val password = binding.etPassword.text.toString()
|
||||||
val confirmPassword = binding.etConfirmPassword.text.toString()
|
val confirmPassword = binding.etConfirmPassword.text.toString()
|
||||||
if (validatePassword(password, confirmPassword)) {
|
if (validatePassword(password, confirmPassword)) {
|
||||||
onPasswordSet(password)
|
viewModel.setPassword(pdfDocument.filePath, password)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,4 +128,7 @@ class PdfSetPasswordDialog(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showToast(message: String) {
|
||||||
|
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
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 androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.all.pdfreader.pro.app.R
|
||||||
|
import com.all.pdfreader.pro.app.databinding.DialogProgressBinding
|
||||||
|
|
||||||
|
class ProgressDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: DialogProgressBinding
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = DialogProgressBinding.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)
|
||||||
|
isCancelable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -128,7 +128,6 @@ class RenameDialogFragment() : DialogFragment() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun showToast(message: String) {
|
private fun showToast(message: String) {
|
||||||
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.pdf.PdfRenderer
|
import android.graphics.pdf.PdfRenderer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -30,6 +31,7 @@ import java.io.FileOutputStream
|
|||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.print.PrintHelper
|
import androidx.print.PrintHelper
|
||||||
import com.all.pdfreader.pro.app.ui.adapter.PrintPdfAdapter
|
import com.all.pdfreader.pro.app.ui.adapter.PrintPdfAdapter
|
||||||
|
import com.shockwave.pdfium.PdfDocument
|
||||||
import com.shockwave.pdfium.PdfPasswordException
|
import com.shockwave.pdfium.PdfPasswordException
|
||||||
import com.shockwave.pdfium.PdfiumCore
|
import com.shockwave.pdfium.PdfiumCore
|
||||||
import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
|
import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
|
||||||
@ -167,4 +169,52 @@ object AppUtils {
|
|||||||
e3.printStackTrace()
|
e3.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun generateFastThumbnail(
|
||||||
|
context: Context,
|
||||||
|
pdfFile: File,
|
||||||
|
maxWidth: Int = 200,
|
||||||
|
maxHeight: Int = 300
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
val pdfiumCore = PdfiumCore(context)
|
||||||
|
val fd = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||||
|
val pdfDocument: PdfDocument = pdfiumCore.newDocument(fd)
|
||||||
|
|
||||||
|
// 打开第一页
|
||||||
|
pdfiumCore.openPage(pdfDocument, 0)
|
||||||
|
|
||||||
|
// 获取原始页宽高
|
||||||
|
val width = pdfiumCore.getPageWidthPoint(pdfDocument, 0)
|
||||||
|
val height = pdfiumCore.getPageHeightPoint(pdfDocument, 0)
|
||||||
|
|
||||||
|
// 计算缩略图尺寸
|
||||||
|
val scale = minOf(maxWidth.toFloat() / width, maxHeight.toFloat() / height)
|
||||||
|
val thumbWidth = (width * scale).toInt()
|
||||||
|
val thumbHeight = (height * scale).toInt()
|
||||||
|
|
||||||
|
val bitmap = createBitmap(thumbWidth, thumbHeight)
|
||||||
|
bitmap.eraseColor(Color.WHITE) // 白底
|
||||||
|
|
||||||
|
// 渲染第一页到 Bitmap
|
||||||
|
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, 0, 0, 0, thumbWidth, thumbHeight)
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
val cacheDir = File(context.cacheDir, "thumbnails")
|
||||||
|
if (!cacheDir.exists()) cacheDir.mkdirs()
|
||||||
|
val thumbFile = File(cacheDir, pdfFile.nameWithoutExtension + ".jpg")
|
||||||
|
FileOutputStream(thumbFile).use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfiumCore.closeDocument(pdfDocument)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
thumbFile.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -529,4 +529,29 @@ object FileUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun duplicateFile(originalFile: File): File? {
|
||||||
|
if (!originalFile.exists()) return null
|
||||||
|
|
||||||
|
val parentDir = originalFile.parentFile ?: return null
|
||||||
|
val fileName = originalFile.nameWithoutExtension
|
||||||
|
val extension = originalFile.extension
|
||||||
|
|
||||||
|
// 自动生成不重复的新文件名
|
||||||
|
var newFile = File(parentDir, "$fileName (copy).$extension")
|
||||||
|
var index = 1
|
||||||
|
while (newFile.exists()) {
|
||||||
|
newFile = File(parentDir, "$fileName ($index).$extension")
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行复制
|
||||||
|
originalFile.inputStream().use { input ->
|
||||||
|
newFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFile
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -9,6 +9,7 @@ import androidx.core.graphics.createBitmap
|
|||||||
import com.all.pdfreader.pro.app.PRApp
|
import com.all.pdfreader.pro.app.PRApp
|
||||||
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.AppUtils.generateFastThumbnail
|
||||||
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
|
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
|
||||||
import com.shockwave.pdfium.PdfDocument
|
import com.shockwave.pdfium.PdfDocument
|
||||||
import com.shockwave.pdfium.PdfiumCore
|
import com.shockwave.pdfium.PdfiumCore
|
||||||
@ -273,51 +274,4 @@ class PdfScanner(
|
|||||||
val lastScan = getLastScanTime()
|
val lastScan = getLastScanTime()
|
||||||
return TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
|
return TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateFastThumbnail(
|
|
||||||
context: Context,
|
|
||||||
pdfFile: File,
|
|
||||||
maxWidth: Int = 200,
|
|
||||||
maxHeight: Int = 300
|
|
||||||
): String? {
|
|
||||||
return try {
|
|
||||||
val pdfiumCore = PdfiumCore(context)
|
|
||||||
val fd = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
||||||
val pdfDocument: PdfDocument = pdfiumCore.newDocument(fd)
|
|
||||||
|
|
||||||
// 打开第一页
|
|
||||||
pdfiumCore.openPage(pdfDocument, 0)
|
|
||||||
|
|
||||||
// 获取原始页宽高
|
|
||||||
val width = pdfiumCore.getPageWidthPoint(pdfDocument, 0)
|
|
||||||
val height = pdfiumCore.getPageHeightPoint(pdfDocument, 0)
|
|
||||||
|
|
||||||
// 计算缩略图尺寸
|
|
||||||
val scale = minOf(maxWidth.toFloat() / width, maxHeight.toFloat() / height)
|
|
||||||
val thumbWidth = (width * scale).toInt()
|
|
||||||
val thumbHeight = (height * scale).toInt()
|
|
||||||
|
|
||||||
val bitmap = createBitmap(thumbWidth, thumbHeight)
|
|
||||||
bitmap.eraseColor(Color.WHITE) // 白底
|
|
||||||
|
|
||||||
// 渲染第一页到 Bitmap
|
|
||||||
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, 0, 0, 0, thumbWidth, thumbHeight)
|
|
||||||
|
|
||||||
// 保存到缓存
|
|
||||||
val cacheDir = File(context.cacheDir, "thumbnails")
|
|
||||||
if (!cacheDir.exists()) cacheDir.mkdirs()
|
|
||||||
val thumbFile = File(cacheDir, pdfFile.nameWithoutExtension + ".jpg")
|
|
||||||
FileOutputStream(thumbFile).use { out ->
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfiumCore.closeDocument(pdfDocument)
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
thumbFile.absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
|
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||||
|
import com.tom_roush.pdfbox.pdmodel.encryption.AccessPermission
|
||||||
|
import com.tom_roush.pdfbox.pdmodel.encryption.StandardProtectionPolicy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object PdfSecurityUtils {
|
||||||
|
|
||||||
|
fun setPasswordToPdf(
|
||||||
|
inputFilePath: String,
|
||||||
|
userPassword: String,
|
||||||
|
ownerPassword: String
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
PDDocument.load(File(inputFilePath)).use { document ->
|
||||||
|
val accessPermission = AccessPermission()
|
||||||
|
val protectionPolicy =
|
||||||
|
StandardProtectionPolicy(ownerPassword, userPassword, accessPermission)
|
||||||
|
protectionPolicy.encryptionKeyLength = 256
|
||||||
|
protectionPolicy.permissions = accessPermission
|
||||||
|
document.protect(protectionPolicy)
|
||||||
|
document.save(inputFilePath)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePasswordFromPdf(inputFilePath: String, password: String): Boolean {
|
||||||
|
return try {
|
||||||
|
PDDocument.load(File(inputFilePath), password).use { document ->
|
||||||
|
document.isAllSecurityToBeRemoved = true // 移除所有安全设置,包括密码
|
||||||
|
document.save(inputFilePath)
|
||||||
|
}
|
||||||
|
true // 返回成功解密的标志
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
false // 返回解密失败的标志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.all.pdfreader.pro.app.viewmodel
|
package com.all.pdfreader.pro.app.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -8,9 +9,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.AppUtils.generateFastThumbnail
|
||||||
import com.all.pdfreader.pro.app.util.FileDeleteUtil
|
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.FileUtils.isPdfEncrypted
|
||||||
|
import com.all.pdfreader.pro.app.util.PdfMetadataExtractor
|
||||||
|
import com.all.pdfreader.pro.app.util.PdfSecurityUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -62,9 +66,7 @@ class PdfViewModel : ViewModel() {
|
|||||||
pdfRepository.updateFilePathAndFileName(filePath, newFilePath, finalName)
|
pdfRepository.updateFilePathAndFileName(filePath, newFilePath, finalName)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
_fileActionEvent.postValue(FileActionEvent.Rename(renameResult))
|
||||||
_fileActionEvent.postValue(FileActionEvent.Rename(renameResult))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,18 +79,76 @@ class PdfViewModel : ViewModel() {
|
|||||||
Log.d("ocean", "文件已删除,清除数据库数据")
|
Log.d("ocean", "文件已删除,清除数据库数据")
|
||||||
pdfRepository.deleteDocument(filePath)
|
pdfRepository.deleteDocument(filePath)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
_fileActionEvent.postValue(FileActionEvent.Delete(deleteResult))
|
||||||
_fileActionEvent.postValue(FileActionEvent.Delete(deleteResult))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCollectState(filePath: String, isFavorite: Boolean) {
|
fun saveCollectState(filePath: String, isFavorite: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
pdfRepository.updateFavoriteStatus(filePath, isFavorite)
|
pdfRepository.updateFavoriteStatus(filePath, isFavorite)
|
||||||
withContext(Dispatchers.Main) {
|
_fileActionEvent.postValue(FileActionEvent.Favorite(isFavorite))
|
||||||
_fileActionEvent.postValue(FileActionEvent.Favorite(isFavorite))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun duplicateFile(context: Context, filePath: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val file = FileUtils.duplicateFile(File(filePath))
|
||||||
|
Log.d("ocean", "duplicateFile->$file")
|
||||||
|
if (file != null) {
|
||||||
|
val isPassword = isPdfEncrypted(file)
|
||||||
|
val metadata = PdfMetadataExtractor.extractMetadata(file.absolutePath)
|
||||||
|
val document = PdfDocumentEntity(
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
fileName = file.name,
|
||||||
|
fileSize = file.length(),
|
||||||
|
lastModified = file.lastModified(),
|
||||||
|
pageCount = metadata?.pageCount ?: 0,
|
||||||
|
thumbnailPath = null,
|
||||||
|
metadataTitle = metadata?.title,
|
||||||
|
metadataAuthor = metadata?.author,
|
||||||
|
metadataSubject = metadata?.subject,
|
||||||
|
metadataKeywords = metadata?.keywords,
|
||||||
|
metadataCreationDate = metadata?.creationDate?.time,
|
||||||
|
metadataModificationDate = metadata?.modificationDate?.time,
|
||||||
|
isPassword = isPassword
|
||||||
|
)
|
||||||
|
pdfRepository.insertOrUpdateDocument(document)
|
||||||
|
Log.d("ocean", "✅ 已保存到数据库: ${file.name}")
|
||||||
|
if (!isPassword) {//没有密码的情况下才去获取缩略图
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val newThumbnail = generateFastThumbnail(context, file)
|
||||||
|
if (newThumbnail != null && document.thumbnailPath != newThumbnail) {
|
||||||
|
pdfRepository.updateThumbnailPath(
|
||||||
|
document.filePath,
|
||||||
|
newThumbnail
|
||||||
|
)
|
||||||
|
Log.d("ocean", "✅ 缩略图已更新")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_fileActionEvent.postValue(FileActionEvent.Duplicate(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPassword(filePath: String, password: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// 发送 START 事件,UI 显示进度框
|
||||||
|
_fileActionEvent.postValue(FileActionEvent.SetPassword(FileActionEvent.SetPassword.Status.START))
|
||||||
|
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
PdfSecurityUtils.setPasswordToPdf(filePath, password, password).also {
|
||||||
|
if (it) {
|
||||||
|
pdfRepository.updateIsPassword(filePath, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_fileActionEvent.postValue(
|
||||||
|
FileActionEvent.SetPassword(
|
||||||
|
FileActionEvent.SetPassword.Status.COMPLETE,
|
||||||
|
success
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- 背景轨道 -->
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="6dp" />
|
||||||
|
<solid android:color="#E0E0E0"/> <!-- 背景色 -->
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- 动画前景 -->
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="6dp"/>
|
||||||
|
<solid android:color="#3F51B5"/> <!-- 动画颜色 -->
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@ -230,6 +230,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/duplicateFileBtn"
|
||||||
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"
|
||||||
@ -251,17 +252,20 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/setPasswordBtn"
|
||||||
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"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/passwordIv"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:src="@drawable/lock" />
|
android:src="@drawable/lock" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/passwordTv"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
|||||||
28
app/src/main/res/layout/dialog_progress.xml
Normal file
28
app/src/main/res/layout/dialog_progress.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/dr_rounded_corner_12_bg_white"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/poppins_medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/processing"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:indeterminate="true"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -49,7 +49,11 @@
|
|||||||
<string name="delete_file">Delete File</string>
|
<string name="delete_file">Delete File</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="set_password">Set Password</string>
|
<string name="set_password">Set Password</string>
|
||||||
|
<string name="set_password_successfully">Set Password successfully</string>
|
||||||
|
<string name="set_password_failed">Set Password failed</string>
|
||||||
<string name="remove_password">Remove Password</string>
|
<string name="remove_password">Remove Password</string>
|
||||||
|
<string name="remove_password_successfully">Remove Password successfully</string>
|
||||||
|
<string name="remove_password_failed">Remove Password failed</string>
|
||||||
<string name="duplicate_file">Duplicate File</string>
|
<string name="duplicate_file">Duplicate File</string>
|
||||||
<string name="enter_name">Enter a name</string>
|
<string name="enter_name">Enter a name</string>
|
||||||
<string name="name_not_empty">File name cannot be empty</string>
|
<string name="name_not_empty">File name cannot be empty</string>
|
||||||
@ -88,4 +92,7 @@
|
|||||||
<string name="pdf_cant_print_password_protected">Cannot print a password protected PDF file</string>
|
<string name="pdf_cant_print_password_protected">Cannot print a password protected PDF file</string>
|
||||||
<string name="cannot_print_malformed_pdf">Cannot print a malformed PDF file</string>
|
<string name="cannot_print_malformed_pdf">Cannot print a malformed PDF file</string>
|
||||||
<string name="pdf_cannot_print_error">Cannot print PDF file, Unknown error has occurred</string>
|
<string name="pdf_cannot_print_error">Cannot print PDF file, Unknown error has occurred</string>
|
||||||
|
<string name="duplicate_created_successfully">Duplicate file created successfully</string>
|
||||||
|
<string name="duplicate_created_failed">Duplicate file created failed</string>
|
||||||
|
<string name="processing">Processing…</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in New Issue
Block a user