拆分功能完成,包含所有功能。

This commit is contained in:
ocean 2025-09-24 14:33:13 +08:00
parent df84891d91
commit 298585b602
8 changed files with 315 additions and 57 deletions

View File

@ -73,6 +73,12 @@
android:label="@string/app_name"
android:screenOrientation="portrait" />
<activity
android:name=".ui.act.SplitPdfResultActivity"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="portrait" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -22,12 +22,22 @@ import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment
import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime
import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class SplitPdfActivity : BaseActivity() {
override val TAG: String = "SplitPdfActivity"
private var currentViewState: ViewState = ViewState.SPLIT_LIST
private enum class ViewState {
SPLIT_LIST, // 拆分页列表
SPLIT_SELECTED // 已选页面列表
}
companion object {
private const val EXTRA_PDF_PATH = "extra_pdf_path"
@ -110,8 +120,7 @@ class SplitPdfActivity : BaseActivity() {
}
binding.continueNowBtn.setOnClickListener {
val selectedPages = splitList.filter { it.isSelected }.map { it.copy() }
val name =
getString(R.string.split) + "_" + System.currentTimeMillis().toUnderscoreDateTime()
val name = getString(R.string.split) + "_" + System.currentTimeMillis().toUnderscoreDateTime()
val item = PdfSelectedPagesItem(filePath, name, selectedPages)
selectedList.add(item)
selectedPdfAdapter.updateAdapter()
@ -119,20 +128,28 @@ class SplitPdfActivity : BaseActivity() {
updateViewState(true)
}
binding.addBtn.setOnClickListener {
//点击重新添加后选中状态全部置为false全选按钮置为false
binding.continueNowBtn.isEnabled = false//继续按钮不可点击
updateContinueNowBtnState(false)//重置继续按钮背景
adapter.setAllSelected(false)
updateSelectAllState(false)
updateViewState(false)
}
binding.splitBtn.setOnClickListener {
//因为图片做的路径缓存方式所以这里直接传入整个集合到result页处理
val intent = SplitPdfResultActivity.createIntent(this, ArrayList(selectedList))
startActivity(intent)
finish()
}
}
private fun initSplitData(file: File) {
lifecycleScope.launch {
splitList.clear()
PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity) // 先清理旧缓存
// 先切换到 IO 线程执行删除,等待完成
withContext(Dispatchers.IO) {
PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity)
}
// 删除完成后,开始收集数据
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
var firstPageLoaded = false
PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem ->
@ -143,7 +160,7 @@ class SplitPdfActivity : BaseActivity() {
splitList[pageItem.pageIndex] = pageItem
adapter.updateItem(pageItem.pageIndex)
}
if (!firstPageLoaded) {//有数据回来则隐藏loading显示全选按钮
if (!firstPageLoaded) {
binding.loadingRoot.root.visibility = View.GONE
binding.selectAllBtn.visibility = View.VISIBLE
firstPageLoaded = true
@ -176,10 +193,12 @@ class SplitPdfActivity : BaseActivity() {
private fun updateViewState(b: Boolean) {
if (b) {
currentViewState = ViewState.SPLIT_SELECTED
binding.selectAllBtn.visibility = View.GONE//隐藏全选按钮
binding.addBtn.visibility = View.VISIBLE//显示add按钮
binding.title.text = getString(R.string.split_pdf)//设置标题
} else {
currentViewState = ViewState.SPLIT_LIST
binding.selectAllBtn.visibility = View.VISIBLE
binding.addBtn.visibility = View.GONE
binding.title.text =
@ -221,15 +240,19 @@ class SplitPdfActivity : BaseActivity() {
override fun onDestroy() {
super.onDestroy()
splitList.clear()
PdfUtils.clearPdfThumbsCache(this)
}
private fun setupBackPressedCallback() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isSelectedViewShow) {
//使用提示对话框
if (!isSelectedViewShow) {//点击继续后说明已经操过过了不直接finish
isEnabled = false
onBackPressedDispatcher.onBackPressed()
return
}
when (currentViewState) {
ViewState.SPLIT_SELECTED -> {
// 已选页面列表,提示是否退出
PromptDialogFragment(
getString(R.string.exit_split),
getString(R.string.confirm_discard_changes),
@ -238,9 +261,16 @@ class SplitPdfActivity : BaseActivity() {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}).show(supportFragmentManager, getString(R.string.exit_split))
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
ViewState.SPLIT_LIST -> {
// 拆分页列表,返回到已选页面列表
adapter.setAllSelected(false)//重置所有选中状态为false
updateSelectAllState(false)//重置选中按钮
binding.continueNowBtn.isEnabled = false//继续按钮不可点击
updateContinueNowBtnState(false)//重置继续按钮背景
updateViewState(true)
}
}
}
})

View File

@ -2,11 +2,17 @@ package com.all.pdfreader.pro.app.ui.act
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
import com.all.pdfreader.pro.app.model.PdfPageItem
@ -14,9 +20,14 @@ import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.PdfSplitResultItem
import com.all.pdfreader.pro.app.ui.act.SplitPdfActivity
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfResultAdapter
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils
import com.all.pdfreader.pro.app.util.PdfScanner
import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class SplitPdfResultActivity : BaseActivity() {
@ -26,11 +37,10 @@ class SplitPdfResultActivity : BaseActivity() {
private const val EXTRA_SELECTED_LIST = "extra_selected_list"
fun createIntent(
context: Context,
selectedPageIndices: List<Int>
context: Context, list: ArrayList<PdfSelectedPagesItem>
): Intent {
return Intent(context, SplitPdfResultActivity::class.java).apply {
putParcelableArrayListExtra(EXTRA_SELECTED_LIST, list)
}
}
}
@ -38,15 +48,24 @@ class SplitPdfResultActivity : BaseActivity() {
private lateinit var binding: ActivityPdfSplitResultBinding
private lateinit var adapter: SplitPdfResultAdapter
private var splitResultList: MutableList<PdfSplitResultItem> = mutableListOf()
private lateinit var selectedList: ArrayList<PdfSelectedPagesItem>
private var isSplitting = false
private var exitDialog: PromptDialogFragment? = null
private val pdfRepository = getRepository()
private lateinit var pdfScanner: PdfScanner
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPdfSplitResultBinding.inflate(layoutInflater)
setContentView(binding.root)
setupBackPressedCallback()
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init()
selectedList = requireParcelableArrayList(EXTRA_SELECTED_LIST)
pdfScanner = PdfScanner(this, pdfRepository)
initView()
setupClick()
initData()
}
private fun initView() {
@ -55,25 +74,80 @@ class SplitPdfResultActivity : BaseActivity() {
binding.recyclerView.adapter = adapter
}
private fun startSplittingPDF(
inputFile: File, selectedPages: List<PdfPageItem>, outputDir: File, outputFileName: String
) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val resultFile = PdfUtils.exportSelectedPages(
private fun initData() {
lifecycleScope.launch(Dispatchers.IO) {
val totalPages = selectedList.sumOf { it.pages.count { it.isSelected } }
var processedPages = 0
withContext(Dispatchers.Main) {
isSplitting = true
binding.splittingLayout.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0
binding.progressBar.max = 100
}
for (item in selectedList) {
val selectedPages = item.pages.filter { it.isSelected }
if (selectedPages.isEmpty()) continue
val inputFile = File(item.filePath)
val outputDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
"PDFReaderPro/split"
).apply { if (!exists()) mkdirs() }
PdfUtils.exportSelectedPages(
inputFile = inputFile,
selectedPages = selectedPages,
outputDir = outputDir,
outputFileName = outputFileName,
onProgress = { current, total ->
})
outputFileName = "${item.fileName}.pdf",
onProgress = { _, _ -> // 不需要单文件百分比
processedPages++// 每页处理完成就加一,多个 PDF 顺序处理时,总进度线性递增
val percent = (processedPages.toFloat() / totalPages * 100).toInt()
lifecycleScope.launch(Dispatchers.Main) {
binding.progressTv.text = "$percent"
binding.progressBar.progress = percent
}
})?.let { resultFile ->
val thumbnails =
AppUtils.generateFastThumbnail(this@SplitPdfResultActivity, resultFile)
val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) {
splitResultList.add(result)
}
}
}
withContext(Dispatchers.Main) {
binding.splittingLayout.visibility = View.GONE
// 默认选中第一个
if (splitResultList.isNotEmpty() && splitResultList.none { it.isSelected }) {
splitResultList[0].isSelected = true
}
adapter.updateAdapter()
isSplitting = false//拆分结束
exitDialog?.dismissAllowingStateLoss()
exitDialog = null
}
}
}
private fun setupClick() {
binding.backBtn.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.shareBtn.setOnClickListener {
val selectedItem = adapter.getSelectedItem()
selectedItem?.let {
AppUtils.shareFile(this, File(selectedItem.filePath))
}
}
binding.okBtn.setOnClickListener {
val selectedItem = adapter.getSelectedItem()
selectedItem?.let {
val intent = PdfViewActivity.createIntent(this, selectedItem.filePath)
startActivity(intent)
}
finish()
}
}
@ -82,4 +156,42 @@ class SplitPdfResultActivity : BaseActivity() {
super.onDestroy()
}
private fun setupBackPressedCallback() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isSplitting) {
exitDialog = PromptDialogFragment(
getString(R.string.exit_split),
getString(R.string.confirm_discard_changes),
getString(R.string.discard),
onOkClick = {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
})
exitDialog?.show(supportFragmentManager, getString(R.string.exit_split))
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
}
/**
* 通用方法读取必传参数如果为 null 直接 finish
*/
@Suppress("DEPRECATION")
private inline fun <reified T : Parcelable> requireParcelableArrayList(key: String): ArrayList<T> {
val result: ArrayList<T>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(key, T::class.java)
} else {
intent.getParcelableArrayListExtra(key)
}
if (result.isNullOrEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
}
return result ?: arrayListOf()
}
}

View File

@ -43,6 +43,20 @@ class SplitPdfResultAdapter(
} else {
holder.binding.selectIv.visibility = View.INVISIBLE
}
holder.binding.root.setOnClickListener {
val oldSelectedIndex = list.indexOfFirst { it.isSelected }
val newSelectedIndex = holder.bindingAdapterPosition
if (oldSelectedIndex != newSelectedIndex && newSelectedIndex != RecyclerView.NO_POSITION) {
// 取消之前选中
if (oldSelectedIndex >= 0) {
list[oldSelectedIndex].isSelected = false
notifyItemChanged(oldSelectedIndex)
}
// 选中当前
list[newSelectedIndex].isSelected = true
notifyItemChanged(newSelectedIndex)
}
}
}
override fun getItemCount(): Int = list.size
@ -52,7 +66,7 @@ class SplitPdfResultAdapter(
notifyDataSetChanged()
}
fun updateItem(position: Int) = notifyItemChanged(position)
fun removeItem(position: Int) = notifyItemRemoved(position)
fun getSelectedItem(): PdfSplitResultItem? {
return list.firstOrNull { it.isSelected }
}
}

View File

@ -70,8 +70,7 @@ class PdfScanner(
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && doc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
doc.filePath,
newThumbnail
doc.filePath, newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
@ -136,8 +135,7 @@ class PdfScanner(
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && document.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
document.filePath,
newThumbnail
document.filePath, newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
@ -162,8 +160,7 @@ class PdfScanner(
val currentIsPassword = isPdfEncrypted(file)
if (existingDoc.isPassword != currentIsPassword) {
LogUtil.logDebug(TAG, "✅ 密码状态需要更新")
updatedDoc =
updatedDoc.copy(isPassword = currentIsPassword)
updatedDoc = updatedDoc.copy(isPassword = currentIsPassword)
needUpdate = true
}
@ -180,8 +177,7 @@ class PdfScanner(
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && existingDoc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
existingDoc.filePath,
newThumbnail
existingDoc.filePath, newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
@ -194,7 +190,9 @@ class PdfScanner(
val file = File(doc.filePath)
if (!file.exists()) {
// 文件不存在 → 删除数据库记录
LogUtil.logDebug(TAG, "最终过滤:文件不存在 -> ${doc.fileName}, 删除记录")
LogUtil.logDebug(
TAG, "最终过滤:文件不存在 -> ${doc.fileName}, 删除记录"
)
pdfRepository.deleteDocument(doc.filePath)
} else {
LogUtil.logDebug(
@ -240,16 +238,62 @@ class PdfScanner(
}
}
fun shouldScan(): Boolean {
return ScanManager.shouldScan(context)
/**
* 添加单个pdf文件到数据库
*/
suspend fun addNewPdfToDatabase(
pathFile: String,
thumbnailPath: String?,
callback: (Boolean) -> Unit = {}
) {
val file = File(pathFile)
if (!file.exists() || !FileUtils.isPdfFile(file)) {
LogUtil.logDebug(TAG, "文件不存在或不是PDF: $pathFile")
withContext(Dispatchers.Main) {
callback.invoke(false)
}
return
}
fun getLastScanTime(): Long {
return ScanManager.getLastScanTime(context)
try {
withContext(Dispatchers.IO) {
val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath)
if (existingDoc == 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 = thumbnailPath,
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)
LogUtil.logDebug(TAG, "✅ 新PDF已保存到数据库: ${file.name}")
withContext(Dispatchers.Main) {
callback.invoke(true)
}
} else {
Log.e(TAG, "数据库有已有相同的文件")
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ 添加新PDF出错: ${e.message}", e)
withContext(Dispatchers.Main) {
callback.invoke(false)
}
}
}
fun getHoursSinceLastScan(): Long {
val lastScan = getLastScanTime()
return TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
}
}

View File

@ -133,5 +133,4 @@ object PdfUtils {
null
}
}
}

View File

@ -11,6 +11,57 @@
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/splittingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/progressTv"
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/black"
android:textSize="32sp" />
<TextView
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="%"
android:textColor="@color/black"
android:textSize="18sp" />
</LinearLayout>
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/splitting"
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_margin="32dp"
android:indeterminate="true" />
</LinearLayout>
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
@ -122,7 +173,7 @@
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
android:text="@string/open"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>

View File

@ -15,10 +15,11 @@
<string name="permission_notice">Permission is required to access files</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
<string name="open">Open</string>
<string name="sort_by">Sort by</string>
<string name="created_date">Created Date</string>
<string name="path">Path</string>
<string name="path_details">Path %1$s</string>
<string name="path_details">Path: %1$s</string>
<string name="file_name">File Name</string>
<string name="file_size">File Size</string>
<string name="ascending">Ascending</string>
@ -133,4 +134,5 @@
<string name="congratulations">Congratulations</string>
<string name="file_created_success">Your file has been successfully created</string>
<string name="please_select_page">Please select at least one page</string>
<string name="splitting">Splitting…</string>
</resources>