一:添加阅读模式对话框

1.水平与垂直阅读
2.逐页
3.反转颜色(夜晚模式)
二:优化fileActionEvent事件通知写法,需要观察什么事件自己写
三:添加自定义CustomSwitchButton按钮
This commit is contained in:
ocean 2025-09-16 18:43:16 +08:00
parent 18190d15af
commit 038c1bc46b
27 changed files with 1738 additions and 215 deletions

View File

@ -1,5 +1,7 @@
package com.all.pdfreader.pro.app.model package com.all.pdfreader.pro.app.model
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import java.io.File import java.io.File
sealed class FileActionEvent { sealed class FileActionEvent {
@ -16,4 +18,19 @@ sealed class FileActionEvent {
data class RemovePassword(val status: Status, val success: Boolean? = null) : FileActionEvent() { data class RemovePassword(val status: Status, val success: Boolean? = null) : FileActionEvent() {
enum class Status { START, COMPLETE } enum class Status { START, COMPLETE }
} }
//只做通知不携带任何数据使用object。
object NoticeReload : FileActionEvent()
inline fun <reified T : FileActionEvent> LiveData<FileActionEvent>.observeEvent(
owner: LifecycleOwner,
crossinline handler: (T) -> Unit
) {
this.observe(owner) { event ->
if (event is T) {
handler(event)
}
}
}
} }

View File

@ -20,14 +20,33 @@ class AppStore(context: Context) {
key = DOCUMENT_SORT_TYPE, defaultValue = SortConfig.default().toPreferenceString() key = DOCUMENT_SORT_TYPE, defaultValue = SortConfig.default().toPreferenceString()
) )
// 是否开启护眼遮罩
var isEyeCareMode: Boolean by store.boolean( var isEyeCareMode: Boolean by store.boolean(
key = IS_EYE_CARE_MODE, defaultValue = false key = IS_EYE_CARE_MODE, defaultValue = false
) )
// 横or竖
var isVertical: Boolean by store.boolean(
key = IS_VERTICAL, defaultValue = true//默认竖
)
// 是否逐页
var isPageFling: Boolean by store.boolean(
key = IS_PAGE_FLING, defaultValue = false
)
// 是否反转颜色
var isColorInversion: Boolean by store.boolean(
key = IS_COLOR_INVERSION, defaultValue = false
)
companion object { companion object {
private const val FILE_NAME = "prp_sp_name" private const val FILE_NAME = "prp_sp_name"
private const val PERMISSIONS_DIALOG_PROMPT = "permissions_dialog_prompt" private const val PERMISSIONS_DIALOG_PROMPT = "permissions_dialog_prompt"
private const val DOCUMENT_SORT_TYPE = "document_sort_type" private const val DOCUMENT_SORT_TYPE = "document_sort_type"
private const val IS_EYE_CARE_MODE = "is_eye_care_mode" private const val IS_EYE_CARE_MODE = "is_eye_care_mode"
private const val IS_VERTICAL = "is_vertical"
private const val IS_PAGE_FLING = "is_page_fling"
private const val IS_COLOR_INVERSION = "is_color_inversion"
} }
} }

View File

@ -22,6 +22,7 @@ import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation
import com.all.pdfreader.pro.app.util.PdfScanner import com.all.pdfreader.pro.app.util.PdfScanner
import com.all.pdfreader.pro.app.util.StoragePermissionHelper import com.all.pdfreader.pro.app.util.StoragePermissionHelper
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import com.all.pdfreader.pro.app.viewmodel.observeEvent
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -71,75 +72,66 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
private fun initObserve() { private fun initObserve() {
//观察其余操作 //观察其余操作
viewModel.fileActionEvent.observe(this) { event -> viewModel.fileActionEvent.observeEvent<FileActionEvent.Rename>(this) { event ->
when (event) { if (event.renameResult.success) {
is FileActionEvent.Rename -> { showToast(getString(R.string.rename_successfully))
if (event.renameResult.success) { } else {
showToast(getString(R.string.rename_successfully)) showToast(event.renameResult.errorMessage.toString())
}
}
viewModel.fileActionEvent.observeEvent<FileActionEvent.Delete>(this){ event ->
if (event.deleteResult.success) {
showToast(getString(R.string.delete_successfully))
} else {
showToast(event.deleteResult.errorMessage.toString())
}
}
viewModel.fileActionEvent.observeEvent<FileActionEvent.Favorite>(this){ event ->
if (event.isFavorite) {
showToast(getString(R.string.added_to_favorites))
} else {
showToast(getString(R.string.removed_from_favorites))
}
}
viewModel.fileActionEvent.observeEvent<FileActionEvent.Duplicate>(this){ event ->
if (event.file != null) {
showToast(getString(R.string.duplicate_created_successfully))
} else {
showToast(getString(R.string.duplicate_created_failed))
}
}
viewModel.fileActionEvent.observeEvent<FileActionEvent.SetPassword>(this){ event ->
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 { } else {
showToast(event.renameResult.errorMessage.toString()) showToast(getString(R.string.set_password_failed))
} }
} }
}
}
viewModel.fileActionEvent.observeEvent<FileActionEvent.RemovePassword>(this){ event ->
when (event.status) {
FileActionEvent.RemovePassword.Status.START -> {
progressDialog = ProgressDialogFragment()
progressDialog?.show(supportFragmentManager, "progressDialog")
}
is FileActionEvent.Delete -> { FileActionEvent.RemovePassword.Status.COMPLETE -> {
if (event.deleteResult.success) { progressDialog?.dismiss()
showToast(getString(R.string.delete_successfully)) progressDialog = null
if (event.success == true) {
showToast(getString(R.string.remove_password_successfully))
} else { } else {
showToast(event.deleteResult.errorMessage.toString()) showToast(getString(R.string.remove_password_failed))
}
}
is FileActionEvent.Favorite -> {
if (event.isFavorite) {
showToast(getString(R.string.added_to_favorites))
} else {
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))
}
}
}
}
is FileActionEvent.RemovePassword -> {
when (event.status) {
FileActionEvent.RemovePassword.Status.START -> {
progressDialog = ProgressDialogFragment()
progressDialog?.show(supportFragmentManager, "progressDialog")
}
FileActionEvent.RemovePassword.Status.COMPLETE -> {
progressDialog?.dismiss()
progressDialog = null
if (event.success == true) {
showToast(getString(R.string.remove_password_successfully))
} else {
showToast(getString(R.string.remove_password_failed))
}
}
} }
} }
} }

View File

@ -9,10 +9,14 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding
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.ui.dialog.PdfPasswordProtectionDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.ViewModelDialogFragment
import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import com.all.pdfreader.pro.app.viewmodel.observeEvent
import com.github.barteksc.pdfviewer.PDFView
import com.github.barteksc.pdfviewer.listener.OnErrorListener import com.github.barteksc.pdfviewer.listener.OnErrorListener
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
@ -69,6 +73,12 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
finish() finish()
} }
} }
viewModel.fileActionEvent.observeEvent<FileActionEvent.NoticeReload>(this) {
logDebug("observeEvent NoticeReload")
val file = File(pdfDocument.filePath)
loadPdfInternal(file, null)
}
} }
private fun setupOnClick() { private fun setupOnClick() {
@ -80,7 +90,9 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
toggleEyeCareMode(appStore.isEyeCareMode) toggleEyeCareMode(appStore.isEyeCareMode)
} }
binding.viewModelBtn.setOnClickListener { binding.viewModelBtn.setOnClickListener {
ViewModelDialogFragment(pdfDocument.filePath).show(
supportFragmentManager, "ViewModelDialogFragment"
)
} }
} }
@ -102,7 +114,8 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
//PDF 文档加载完成时回调 //PDF 文档加载完成时回调
override fun loadComplete(nbPages: Int) { override fun loadComplete(nbPages: Int) {
//加载完毕进行一次缩放
binding.pdfview.resetZoomWithAnimation()
} }
//PDF 加载出错时回调 //PDF 加载出错时回调
@ -161,20 +174,27 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
} }
private fun loadPdfInternal(file: File, password: String?) { private fun loadPdfInternal(file: File, password: String?) {
binding.pdfview.fromFile(file) binding.pdfview.fromFile(file).apply {
.apply { password?.let { password(it) } // 只有在有密码时才调用
password?.let { password(it) } // 只有在有密码时才调用 defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始
defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始 enableDoubletap(true) // 是否允许双击缩放
enableDoubletap(true) // 是否允许双击缩放 enableAnnotationRendering(true) // 是否渲染注释
enableAnnotationRendering(true) // 是否渲染注释 onLoad(this@PdfViewActivity) // 加载回调
onLoad(this@PdfViewActivity) // 加载回调 onError(this@PdfViewActivity) // 错误回调
onError(this@PdfViewActivity) // 错误回调 onTap(this@PdfViewActivity) // 单击回调
onTap(this@PdfViewActivity) // 单击回调 onPageChange(this@PdfViewActivity) // 页面改变回调
onPageChange(this@PdfViewActivity) // 页面改变回调 scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示
scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示 if (appStore.isPageFling) {
pageFling(true)//逐页滑动 pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。
autoSpacing(true)//开启逐页就开启自动间距
pageFling(true)//逐页
} else {
spacing(10)
pageFling(false)
} }
.load() swipeHorizontal(!appStore.isVertical)
nightMode(appStore.isColorInversion)
}.load()
} }
private fun toggleFullScreen() { private fun toggleFullScreen() {

View File

@ -1,36 +1,32 @@
package com.all.pdfreader.pro.app.ui.dialog package com.all.pdfreader.pro.app.ui.dialog
import android.net.Uri import android.animation.ValueAnimator
import android.graphics.PorterDuff
import android.os.Bundle import android.os.Bundle
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.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.activityViewModels 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.DialogListMoreBinding
import com.all.pdfreader.pro.app.databinding.DialogViewModelBinding import com.all.pdfreader.pro.app.databinding.DialogViewModelBinding
import com.all.pdfreader.pro.app.model.PrintResult
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx import com.all.pdfreader.pro.app.sp.AppStore
import com.all.pdfreader.pro.app.util.AppUtils.printPdfFile import com.all.pdfreader.pro.app.ui.view.CustomSwitchButton
import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation import com.all.pdfreader.pro.app.ui.view.CustomSwitchButton.OnCheckedChangeListener
import com.all.pdfreader.pro.app.util.AppUtils.shareFile
import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize
import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.io.File
class ViewModelDialogFragment(val filePath: String) : BottomSheetDialogFragment() { class ViewModelDialogFragment(val filePath: String) : BottomSheetDialogFragment() {
private lateinit var binding: DialogViewModelBinding private lateinit var binding: DialogViewModelBinding
private val viewModel: PdfViewModel by activityViewModels() private val viewModel: PdfViewModel by activityViewModels()//为PdfViewActivity的PdfViewModel
private lateinit var pdfDocument: PdfDocumentEntity private lateinit var pdfDocument: PdfDocumentEntity
private var isFavorite: Boolean = false private var isFirstSetSelected = true// 第一次直接设置位置,不做指示器的切换动画
private val appstore by lazy { AppStore(requireActivity()) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -59,127 +55,109 @@ class ViewModelDialogFragment(val filePath: String) : BottomSheetDialogFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.pdfDocument.observe(this) { document -> viewModel.pdfDocument.value?.let {
document?.let { pdfDocument = it
pdfDocument = it initUi()
isFavorite = pdfDocument.isFavorite setupOnClick()
initUi() } ?: run {
setupOnClick() showToast(getString(R.string.file_not))
} ?: run { dismiss()
showToast(getString(R.string.file_not))
dismiss()
}
} }
viewModel.getPDFDocument(filePath)
} }
private fun initUi() { private fun initUi() {
binding.tvFileName.text = pdfDocument.fileName // 初始化指示器宽度
binding.tvFileSize.text = pdfDocument.fileSize.toFormatFileSize() binding.indicator.post {
binding.tvFileDate.text = pdfDocument.lastModified.toSlashDate() val indicatorWidth = binding.btnHorizontal.width
if (pdfDocument.isPassword) { binding.indicator.layoutParams.width = indicatorWidth
binding.lockLayout.visibility = View.VISIBLE binding.indicator.requestLayout()
binding.tvFileImg.visibility = View.GONE setSelectedMode(appstore.isVertical)
} else {
binding.lockLayout.visibility = View.GONE
binding.tvFileImg.visibility = View.VISIBLE
Glide.with(binding.root).load(pdfDocument.thumbnailPath)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(binding.root.context)))
.into(binding.tvFileImg)
} }
updateCollectUi(isFavorite) binding.switchPageByPage.setChecked(appstore.isPageFling)
updatePasswordUi(pdfDocument.isPassword) binding.switchColorInversion.setChecked(appstore.isColorInversion)
} }
private fun setupOnClick() { private fun setupOnClick() {
binding.collectBtn.setClickWithAnimation(duration = 250) { binding.btnHorizontal.setOnClickListener {
isFavorite = !isFavorite setSelectedMode(isVertical = false)
updateCollectUi(isFavorite) AppStore(requireActivity()).isVertical = false
viewModel.saveCollectState(pdfDocument.filePath, isFavorite) viewModel.noticeReloadPdf()
dismiss()
} }
binding.renameFileBtn.setOnClickListener { binding.btnVertical.setOnClickListener {
RenameDialogFragment().show(parentFragmentManager, "ListMoreDialogFragment") setSelectedMode(isVertical = true)
dismiss() AppStore(requireActivity()).isVertical = true
viewModel.noticeReloadPdf()
} }
binding.deleteFileBtn.setOnClickListener { binding.switchPageByPage.setOnCheckedChangeListener(object : OnCheckedChangeListener {
DeleteDialogFragment().show(parentFragmentManager, "DeleteDialogFragment") override fun onCheckedChanged(view: CustomSwitchButton?, isChecked: Boolean) {
dismiss() view?.setChecked(isChecked)
} appstore.isPageFling = isChecked
binding.detailsBtn.setOnClickListener { viewModel.noticeReloadPdf()
FileDetailsDialogFragment().show(parentFragmentManager, "FileDetailsDialogFragment")
dismiss()
}
binding.shareBtn.setOnClickListener {
shareFile(requireActivity(), File(pdfDocument.filePath))
dismiss()
}
binding.printBtn.setOnClickListener {
val result = printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath)))
when (result) {
PrintResult.DeviceNotSupported -> {
Toast.makeText(
context,
R.string.device_does_not_support_printing,
Toast.LENGTH_LONG
).show()
}
is PrintResult.Error -> {
Toast.makeText(context, R.string.pdf_cannot_print_error, Toast.LENGTH_LONG)
.show()
}
PrintResult.MalformedPdf -> {
Toast.makeText(context, R.string.cannot_print_malformed_pdf, Toast.LENGTH_LONG)
.show()
}
PrintResult.PasswordRequired -> {
Toast.makeText(
context,
R.string.pdf_cant_print_password_protected,
Toast.LENGTH_LONG
).show()
}
PrintResult.Success -> {
}
} }
dismiss() })
} binding.switchColorInversion.setOnCheckedChangeListener(object : OnCheckedChangeListener {
binding.duplicateFileBtn.setOnClickListener { override fun onCheckedChanged(
viewModel.duplicateFile(requireActivity(), pdfDocument.filePath) view: CustomSwitchButton?,
dismiss() isChecked: Boolean
} ) {
binding.setPasswordBtn.setOnClickListener { view?.setChecked(isChecked)
if (pdfDocument.isPassword) { appstore.isColorInversion = isChecked
PdfRemovePasswordDialog().show(parentFragmentManager, "PdfRemovePasswordDialog") viewModel.noticeReloadPdf()
}
})
}
private fun setSelectedMode(isVertical: Boolean) {
binding.apply {
val targetX = if (isVertical) btnVertical.x else btnHorizontal.x
if (isFirstSetSelected) {
indicator.translationX = targetX
isFirstSetSelected = false
} else { } else {
PdfSetPasswordDialog().show(parentFragmentManager, "PdfSetPasswordDialog") indicator.animate().translationX(targetX).setDuration(250)
.setInterpolator(AccelerateDecelerateInterpolator()).start()
} }
dismiss()
// 横向
animateIconAndTextColor(
horizontalTv,
horizontalIv,
if (isVertical) requireContext().getColor(R.color.white)
else requireContext().getColor(R.color.icon_color),
if (isVertical) requireContext().getColor(R.color.icon_color)
else requireContext().getColor(R.color.white)
)
// 垂直
animateIconAndTextColor(
verticalTv,
verticalIv,
if (isVertical) requireContext().getColor(R.color.icon_color)
else requireContext().getColor(R.color.white),
if (isVertical) requireContext().getColor(R.color.white)
else requireContext().getColor(R.color.icon_color)
)
} }
} }
private fun updateCollectUi(b: Boolean) {
if (b) { /**
binding.collectIv.setImageResource(R.drawable.collected) * 改变img与text的颜色值动画渐变同步indicator的时间
} else { */
binding.collectIv.setImageResource(R.drawable.collect) private fun animateIconAndTextColor(
textView: TextView, imageView: ImageView, fromColor: Int, toColor: Int
) {
ValueAnimator.ofArgb(fromColor, toColor).apply {
duration = 250
addUpdateListener { animator ->
val animatedColor = animator.animatedValue as Int
textView.setTextColor(animatedColor)
imageView.setColorFilter(animatedColor, PorterDuff.Mode.SRC_IN)
}
start()
} }
} }
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()

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,18 @@
package com.all.pdfreader.pro.app.util package com.all.pdfreader.pro.app.util
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
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.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.PdfiumCore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class PdfScanner( class PdfScanner(

View File

@ -0,0 +1,19 @@
package com.all.pdfreader.pro.app.viewmodel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.all.pdfreader.pro.app.model.FileActionEvent
/**
* 公用观察FileActionEvent事件函数只观察需要观察的响应
*/
inline fun <reified T : FileActionEvent> LiveData<FileActionEvent>.observeEvent(
owner: LifecycleOwner,
crossinline handler: (T) -> Unit
) {
this.observe(owner) { event ->
if (event is T) {
handler(event)
}
}
}

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.all.pdfreader.pro.app.PRApp
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
@ -156,19 +157,40 @@ class PdfViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
_fileActionEvent.postValue(FileActionEvent.RemovePassword(FileActionEvent.RemovePassword.Status.START)) _fileActionEvent.postValue(FileActionEvent.RemovePassword(FileActionEvent.RemovePassword.Status.START))
val success = withContext(Dispatchers.IO) { val success = try {
PdfSecurityUtils.removePasswordFromPdf(filePath, password).also { withContext(Dispatchers.IO) {
if (it) { val removed = PdfSecurityUtils.removePasswordFromPdf(filePath, password)
Log.d("ocean", "密码移除$removed")
if (removed) {
pdfRepository.updateIsPassword(filePath, false) pdfRepository.updateIsPassword(filePath, false)
val document = pdfRepository.getDocumentByPath(filePath)
if (document?.thumbnailPath.isNullOrEmpty()) {
val newThumbnail = generateFastThumbnail(PRApp.getContext(), File(filePath))
Log.d("ocean", "最新图片:$newThumbnail")
if (!newThumbnail.isNullOrEmpty() && filePath != newThumbnail) {
pdfRepository.updateThumbnailPath(filePath, newThumbnail)
}
}
} }
removed // 这里直接返回移除密码的结果
} }
} catch (e: Exception) {
Log.e("ocean", "removePassword 失败", e)
false
} }
_fileActionEvent.postValue( _fileActionEvent.postValue(
FileActionEvent.SetPassword( FileActionEvent.RemovePassword(
FileActionEvent.SetPassword.Status.COMPLETE, FileActionEvent.RemovePassword.Status.COMPLETE,
success success
) )
) )
} }
} }
fun noticeReloadPdf() {
viewModelScope.launch {
_fileActionEvent.postValue(FileActionEvent.NoticeReload)
}
}
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 滑块颜色 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#C8E6C9" android:state_checked="true"/> <!---->
<item android:color="#E0E0E0" android:state_checked="false"/> <!---->
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 轨道颜色 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#4CAF50" android:state_checked="true"/> <!---->
<item android:color="#9E9E9E" android:state_checked="false"/> <!---->
</selector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillAlpha="0.01"
android:fillColor="@color/icon_color"
android:pathData="M0,0h24v24h-24z"
android:strokeAlpha="0.01" />
<path
android:fillColor="@color/icon_color"
android:fillType="evenOdd"
android:pathData="M2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12ZM20,12C20,7.582 16.418,4 12,4C7.582,4 4,7.582 4,12C4,16.418 7.582,20 12,20C16.418,20 20,16.418 20,12Z" />
<path
android:fillColor="@color/icon_color"
android:fillType="evenOdd"
android:pathData="M12,18C15.314,18 18,15.314 18,12C18,8.686 15.314,6 12,6V18Z" />
</vector>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/grey"/>
<corners android:radius="24dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/black"/>
<corners android:radius="24dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="24dp"/>
<solid android:color="@android:color/transparent"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#000000"/>
<corners android:radius="24dp"/>
</shape>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M5.5,4L18.5,4C18.7761,4 19,4.2239 19,4.5L19,15.7929C19,15.9255 18.9473,16.0527 18.8536,16.1464L15.1464,19.8536C15.0527,19.9473 14.9255,20 14.7929,20L5.5,20C5.2239,20 5,19.7761 5,19.5L5,4.5C5,4.2239 5.2239,4 5.5,4Z"
android:strokeWidth="2.0"
android:strokeColor="@color/icon_color" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M18,15L14.5,15C14.2239,15 14,15.2239 14,15.5L14,20L14,20"
android:strokeWidth="2.0"
android:strokeColor="@color/icon_color"
android:strokeLineCap="square" />
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,7h16v10h-16z"
android:strokeWidth="2"
android:strokeColor="@color/icon_color"
android:fillColor="@android:color/transparent"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7,4h10v16h-10z"
android:strokeWidth="2"
android:strokeColor="@color/icon_color"
android:fillColor="@android:color/transparent"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
</vector>

View File

@ -44,12 +44,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@color/white"> android:gravity="center">
<com.github.barteksc.pdfviewer.PDFView <com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfview" android:id="@+id/pdfview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:background="@color/grey" />
</LinearLayout> </LinearLayout>

View File

@ -246,7 +246,6 @@
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"
android:fontFamily="@font/poppins_medium"
android:text="@string/duplicate_file" android:text="@string/duplicate_file"
android:textColor="@color/grey_text_color" android:textColor="@color/grey_text_color"
android:textSize="16sp" /> android:textSize="16sp" />
@ -270,7 +269,6 @@
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"
android:fontFamily="@font/poppins_medium"
android:text="@string/set_password" android:text="@string/set_password"
android:textColor="@color/grey_text_color" android:textColor="@color/grey_text_color"
android:textSize="16sp" /> android:textSize="16sp" />
@ -292,7 +290,6 @@
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"
android:fontFamily="@font/poppins_medium"
android:text="@string/delete_file" android:text="@string/delete_file"
android:textColor="@color/grey_text_color" android:textColor="@color/grey_text_color"
android:textSize="16sp" /> android:textSize="16sp" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
@ -17,4 +18,161 @@
android:background="@drawable/dr_dialog_indicator_bg" /> android:background="@drawable/dr_dialog_indicator_bg" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/poppins_semibold"
android:text="@string/view_model"
android:textColor="@color/black"
android:textSize="18sp" />
<FrameLayout
android:id="@+id/toggleContainer"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/dr_bg_toggle_group"
android:clipToPadding="false">
<View
android:id="@+id/indicator"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@drawable/dr_bg_toggle_indicator"
android:translationX="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/btnHorizontal"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/horizontalIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/read_horizontal" />
<TextView
android:id="@+id/horizontalTv"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:text="@string/horizontal"
android:textColor="@color/icon_color"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnVertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/verticalIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/read_vertical" />
<TextView
android:id="@+id/verticalTv"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:text="@string/vertical"
android:textColor="@color/icon_color"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/page_by_page" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/page_by_page"
android:textColor="@color/icon_color"
android:textSize="16sp" />
<com.all.pdfreader.pro.app.ui.view.CustomSwitchButton
android:id="@+id/switchPageByPage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:sb_background="@color/grey"
app:sb_show_indicator="false" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/color_inversion" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/color_inversion"
android:textColor="@color/icon_color"
android:textSize="16sp" />
<com.all.pdfreader.pro.app.ui.view.CustomSwitchButton
android:id="@+id/switchColorInversion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:sb_background="@color/grey"
app:sb_show_indicator="false" />
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -15,4 +15,5 @@
<color name="black_img_color">#2c2c2c</color> <color name="black_img_color">#2c2c2c</color>
<color name="grey_text_color">#666666</color> <color name="grey_text_color">#666666</color>
<color name="eye_protection_color">#80FFD699</color> <color name="eye_protection_color">#80FFD699</color>
<color name="icon_color">#636366</color>
</resources> </resources>

View File

@ -97,4 +97,8 @@
<string name="duplicate_created_failed">Duplicate file created failed</string> <string name="duplicate_created_failed">Duplicate file created failed</string>
<string name="processing">Processing…</string> <string name="processing">Processing…</string>
<string name="view_model">View Model</string> <string name="view_model">View Model</string>
<string name="page_by_page">Page by page</string>
<string name="color_inversion">color inversion</string>
<string name="horizontal">Horizontal</string>
<string name="vertical">Vertical</string>
</resources> </resources>

View File

@ -17,7 +17,7 @@
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>
</style> </style>
<style name="CustomBottomSheetDialogTheme" parent="@style/Theme.Design.BottomSheetDialog"> <style name="CustomBottomSheetDialogTheme" parent="@style/Theme.MaterialComponents.Light.BottomSheetDialog">
<!-- 关键属性:取消浮动效果 --> <!-- 关键属性:取消浮动效果 -->
<item name="android:windowIsFloating">false</item> <item name="android:windowIsFloating">false</item>
<!-- 设置导航栏颜色 --> <!-- 设置导航栏颜色 -->
@ -31,4 +31,18 @@
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
</style> </style>
<style name="TextViewFont_PopMedium">
<item name="android:includeFontPadding">false</item>
<item name="android:fontFamily">@font/poppins_medium</item>
</style>
<style name="TextViewFont_PopRegular">
<item name="android:includeFontPadding">false</item>
<item name="android:fontFamily">@font/poppins_regular</item>
</style>
<style name="TextViewFont_PopSemiBold">
<item name="android:includeFontPadding">false</item>
<item name="android:fontFamily">@font/poppins_semibold</item>
</style>
</resources> </resources>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwitchButton">
<attr name="sb_shadow_radius" format="reference|dimension"/>
<attr name="sb_shadow_offset" format="reference|dimension"/>
<attr name="sb_shadow_color" format="reference|color"/>
<attr name="sb_uncheck_color" format="reference|color"/>
<attr name="sb_checked_color" format="reference|color"/>
<attr name="sb_border_width" format="reference|dimension"/>
<attr name="sb_checkline_color" format="reference|color"/>
<attr name="sb_checkline_width" format="reference|dimension"/>
<attr name="sb_uncheckcircle_color" format="reference|color"/>
<attr name="sb_uncheckcircle_width" format="reference|dimension"/>
<attr name="sb_uncheckcircle_radius" format="reference|dimension"/>
<attr name="sb_checked" format="reference|boolean"/>
<attr name="sb_shadow_effect" format="reference|boolean"/>
<attr name="sb_effect_duration" format="reference|integer"/>
<attr name="sb_button_color" format="reference|color"/>
<attr name="sb_show_indicator" format="reference|boolean"/>
<attr name="sb_background" format="reference|color"/>
<attr name="sb_enable_effect" format="reference|boolean"/>
<attr name="sb_checkedbutton_color" format="reference|color"/>
<attr name="sb_uncheckbutton_color" format="reference|color"/>
</declare-styleable>
</resources>

View File

@ -19,6 +19,7 @@ swiperefreshlayout = "1.1.0"
recyclerview = "1.4.0" recyclerview = "1.4.0"
protoliteWellKnownTypes = "18.0.1" protoliteWellKnownTypes = "18.0.1"
material = "1.12.0" material = "1.12.0"
composeMaterial3 = "1.5.1"
[libraries] [libraries]
androidpdfviewer = { module = "com.github.marain87:AndroidPdfViewer", version.ref = "androidpdfviewer" } androidpdfviewer = { module = "com.github.marain87:AndroidPdfViewer", version.ref = "androidpdfviewer" }
@ -40,6 +41,7 @@ androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview"
pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" } pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" }
protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }