diff --git a/.idea/misc.xml b/.idea/misc.xml index 274ccf3..14be325 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15231ad..73ff0cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,6 +85,18 @@ android:label="@string/app_name" android:screenOrientation="portrait" /> + + + + (activeFragment as HomeFrag).adapter.getSelectedItems() + is FavoriteFrag -> (activeFragment as FavoriteFrag).adapter.getSelectedItems() + is RecentlyFrag -> (activeFragment as RecentlyFrag).adapter.getSelectedItems() + else -> emptyList() + } + if (selectedItems.isNotEmpty()) { + val intent = MergePdfActivity.createIntent(this, ArrayList(selectedItems)) + startActivity(intent) + exitAllMultiSelect() + } } binding.multiSelectRemoveBtn.setOnSingleClickListener { val selectedItems = recentlyFragment.adapter.getSelectedItems() diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MergePdfActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MergePdfActivity.kt new file mode 100644 index 0000000..030e25a --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MergePdfActivity.kt @@ -0,0 +1,171 @@ +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.Parcelable +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContract +import androidx.recyclerview.widget.LinearLayoutManager +import com.all.pdfreader.pro.app.R +import com.all.pdfreader.pro.app.databinding.ActivityPdfMergeBinding +import com.all.pdfreader.pro.app.model.PdfPickerSource +import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity +import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter +import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment +import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation +import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener +import com.gyf.immersionbar.ImmersionBar + +class MergePdfActivity : BaseActivity() { + override val TAG: String = "MergePdfActivity" + private lateinit var binding: ActivityPdfMergeBinding + private lateinit var selectedList: ArrayList + private lateinit var adapter: PdfAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPdfMergeBinding.inflate(layoutInflater) + setContentView(binding.root) + setupBackPressedCallback() + ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) + .navigationBarColor(R.color.bg_color).init() + selectedList = requireParcelableArrayList(EXTRA_PDF_LIST) + updateContinueNowBtnState(selectedList.size >= 2) + initView() + setupClick() + } + + private fun initView() { + adapter = PdfAdapter( + pdfList = selectedList, + onDeleteItemClick = { item, position -> + adapter.removeItem(position) + selectedList.remove(item) + updateContinueNowBtnState(selectedList.size >= 2) + }, + enableLongClick = false, + showMoreButton = false, + showCheckButton = false, + isMultiSelectMode = false, + showDeleteButton = true + ) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + } + + private fun setupClick() { + binding.backBtn.setOnSingleClickListener { onBackPressedDispatcher.onBackPressed() } + binding.addBtn.setClickWithAnimation { + openPicker() + } + binding.continueNowBtn.setOnSingleClickListener { + val list = selectedList.map { it.filePath } + val intent = SplitPdfResultActivity.createIntentInputFile( + this, ArrayList(list), PdfPickerSource.MERGE + ) + startActivity(intent) + finish() + } + } + + private val pickPdfLauncher = + registerForActivityResult(PickPdfContract(Companion.TAG)) { list -> + if (list.isNotEmpty()) { + handleSelectedPdfs(list) + } + } + + private fun openPicker() { + pickPdfLauncher.launch(PdfPickerSource.MERGE to selectedList) + } + + private fun handleSelectedPdfs(list: List) { + selectedList.clear() + selectedList.addAll(list) + adapter.notifyDataSetChanged() + updateContinueNowBtnState(selectedList.size >= 2) + } + + private fun updateContinueNowBtnState(b: Boolean) { + binding.continueNowBtn.setBackgroundResource( + if (b) R.drawable.dr_click_btn_bg + else R.drawable.dr_btn_not_clickable_bg + ) + binding.continueNowBtn.isEnabled = b + } + + private fun setupBackPressedCallback() { + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + PromptDialogFragment( + getString(R.string.exit_split), + getString(R.string.confirm_discard_changes), + getString(R.string.discard), + onOkClick = { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + }).show(supportFragmentManager, getString(R.string.exit_split)) + } + }) + } + + + companion object { + const val EXTRA_SELECTED_LIST = "extra_selected_list" + const val EXTRA_PDF_LIST = "extra_pdf_list" + const val TAG = "MergePdfActivity" + + fun createIntent(context: Context, list: ArrayList): Intent { + return Intent(context, MergePdfActivity::class.java).apply { + putParcelableArrayListExtra(EXTRA_PDF_LIST, list) + } + } + } + + /** + * 通用方法:读取必传参数,如果为 null 直接 finish + */ + @Suppress("DEPRECATION") + private inline fun requireParcelableArrayList(key: String): ArrayList { + val result: ArrayList? = 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() + } + + class PickPdfContract( + private val from: String + ) : ActivityResultContract>, List>() { + + override fun createIntent( + context: Context, input: Pair> + ): Intent { + val (source, historyList) = input + return PdfPickerActivity.createIntent(context, source, from, historyList) + } + + override fun parseResult(resultCode: Int, intent: Intent?): List { + if (resultCode != RESULT_OK || intent == null) return emptyList() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra( + EXTRA_SELECTED_LIST, PdfDocumentEntity::class.java + ) ?: emptyList() + } else { + @Suppress("DEPRECATION") intent.getParcelableArrayListExtra( + EXTRA_SELECTED_LIST + ) ?: emptyList() + } + } + } + + +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfPickerActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfPickerActivity.kt new file mode 100644 index 0000000..9c32b0c --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfPickerActivity.kt @@ -0,0 +1,218 @@ +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.Parcelable +import android.view.View +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.all.pdfreader.pro.app.R +import com.all.pdfreader.pro.app.databinding.ActivityPdfPickerBinding +import com.all.pdfreader.pro.app.model.PdfPickerSource +import com.all.pdfreader.pro.app.model.SortConfig +import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity +import com.all.pdfreader.pro.app.room.repository.PdfRepository +import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter +import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation +import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener +import com.gyf.immersionbar.ImmersionBar +import kotlinx.coroutines.launch +import java.io.Serializable + +class PdfPickerActivity : BaseActivity() { + override val TAG: String = "PdfPickerActivity" + private lateinit var binding: ActivityPdfPickerBinding + private lateinit var adapter: PdfAdapter + private lateinit var source: PdfPickerSource // 保存来源 + private var showCheckButton = false + private var isMultiSelectMode = false + private var fromActivityResult: String = "" + private lateinit var historyList: ArrayList + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPdfPickerBinding.inflate(layoutInflater) + setContentView(binding.root) + source = getSerializableOrDefault(EXTRA_SOURCE, PdfPickerSource.NONE) + if (source == PdfPickerSource.NONE) { + showToast(getString(R.string.unknown_source)) + finish() + } + fromActivityResult = intent.getStringExtra(EXTRA_FROM) ?: "" + historyList = requireParcelableArrayList(EXTRA_HISTORY_LIST) + ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) + .navigationBarColor(R.color.bg_color).init() + updateViewAndState() + initView() + setupClick() + initData() + } + + private fun updateViewAndState(number: Int = 0) { + if (source == PdfPickerSource.MERGE) { + binding.title.text = getString(R.string.selected_page, number) + binding.continueNowBtn.visibility = View.VISIBLE + updateContinueNowBtnState(number >= 2) + } else { + binding.title.text = getString(R.string.please_select_a_file) + binding.continueNowBtn.visibility = View.GONE + } + } + + private fun initView() { + if (source == PdfPickerSource.MERGE) { + showCheckButton = true + isMultiSelectMode = true + } else { + showCheckButton = false + isMultiSelectMode = false + } + adapter = PdfAdapter( + pdfList = mutableListOf(), + onItemClick = { pdf -> + when (source) { + PdfPickerSource.MERGE -> {} + PdfPickerSource.SPLIT -> { + val intent = SplitPdfActivity.createIntent(this, pdf.filePath) + startActivity(intent) + finish() + } + + PdfPickerSource.LOCK -> {} + PdfPickerSource.TO_IMAGES -> {} + PdfPickerSource.TO_LONG_IMAGE -> {} + PdfPickerSource.NONE -> {} + } + }, + onSelectModelItemClick = { + if (source == PdfPickerSource.MERGE) { + val selectedItems = adapter.getSelectedItems() + if (selectedItems.isNotEmpty()) { + val filePaths = selectedItems.map { it.filePath } + logDebug("${filePaths.size}") + } + updateViewAndState(selectedItems.size) + } + }, + enableLongClick = false, + showMoreButton = false, + showCheckButton = showCheckButton, + isMultiSelectMode = isMultiSelectMode + ) + + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + } + + private fun initData() { + lifecycleScope.launch { + PdfRepository.getInstance().getAllDocuments().collect { list -> + val sortedList = sortDocuments(list) + // 标记已选择项 + if (fromActivityResult == MergePdfActivity.TAG) { + markSelectedItems(sortedList, historyList) + } + if (list.isNotEmpty()) { + adapter.updateData(sortedList) + binding.noFilesLayout.visibility = View.GONE + } else { + binding.noFilesLayout.visibility = View.VISIBLE + } + } + } + } + + private fun markSelectedItems( + sortedList: List, historyList: List + ) { + val historySet = historyList.map { it.filePath }.toSet() + sortedList.forEach { item -> + item.isSelected = item.filePath in historySet + } + } + + private fun setupClick() { + binding.backBtn.setOnSingleClickListener { finish() } + binding.searchBtn.setClickWithAnimation { + + } + binding.continueNowBtn.setOnSingleClickListener { + val selectedItems = adapter.getSelectedItems() + if (selectedItems.size >= 2) { + if (fromActivityResult == MergePdfActivity.TAG) { + returnSelectedResult(ArrayList(selectedItems)) + } else { + val intent = MergePdfActivity.createIntent(this, ArrayList(selectedItems)) + startActivity(intent) + finish() + } + } + } + + } + + private fun returnSelectedResult(selectedList: ArrayList) { + val intent = Intent().apply { + putParcelableArrayListExtra(MergePdfActivity.EXTRA_SELECTED_LIST, selectedList) + } + setResult(RESULT_OK, intent) + finish() + } + + private fun updateContinueNowBtnState(b: Boolean) { + binding.continueNowBtn.setBackgroundResource( + if (b) R.drawable.dr_click_btn_bg + else R.drawable.dr_btn_not_clickable_bg + ) + binding.continueNowBtn.isEnabled = b + } + + + private fun sortDocuments(documents: List): List { + val sortConfig = SortConfig.fromPreferenceString(appStore.documentSortType) + return sortConfig.applySort(documents) + } + + companion object { + private const val EXTRA_SOURCE = "extra_source" + private const val EXTRA_FROM = "extra_from"//从哪儿来 + private const val EXTRA_HISTORY_LIST = "extra_history_list"//已经有选中的数据 + + fun createIntent( + context: Context, + source: PdfPickerSource, + from: String = "", + list: ArrayList = arrayListOf() + ): Intent { + return Intent(context, PdfPickerActivity::class.java).apply { + putExtra(EXTRA_SOURCE, source) + putExtra(EXTRA_FROM, from) + putParcelableArrayListExtra(EXTRA_HISTORY_LIST, list) + } + } + } + + @Suppress("DEPRECATION") + private inline fun requireParcelableArrayList(key: String): ArrayList { + val result: ArrayList? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra(key, T::class.java) + } else { + intent.getParcelableArrayListExtra(key) + } + return result ?: arrayListOf() + } + + private inline fun getSerializableOrDefault( + key: String, default: T + ): T { + val result: T? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(key, T::class.java) + } else { + @Suppress("DEPRECATION") intent.getSerializableExtra(key) as? T + } + return result ?: default + } + +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt index 12fbe2a..3c100c0 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt @@ -215,14 +215,13 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList override fun onError(t: Throwable?) { logDebug("PDF loading error: ${t?.message}") t?.let { - val errorMessage = it.message ?: "未知错误" + val errorMessage = it.message ?: getString(R.string.pdf_loading_failed) // 检查是否是密码相关的错误 if (errorMessage.contains("Password") || errorMessage.contains("password")) { val file = File(pdfDocument.filePath) showPasswordDialog(file) } else { - // 其他错误 - showToast(getString(R.string.pdf_loading_failed)) + showToast(errorMessage) finish() } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt index 526da0a..0c57c36 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitBinding import com.all.pdfreader.pro.app.model.PdfPageItem +import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem import com.all.pdfreader.pro.app.model.RenameType import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter @@ -121,7 +122,8 @@ class SplitPdfActivity : BaseActivity() { } binding.continueNowBtn.setOnSingleClickListener { 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() @@ -136,8 +138,13 @@ class SplitPdfActivity : BaseActivity() { updateViewState(false) } binding.splitBtn.setOnSingleClickListener { + logDebug("${selectedList.size}") //因为图片做的路径缓存方式,所以这里直接传入整个集合到result页处理 - val intent = SplitPdfResultActivity.createIntent(this, ArrayList(selectedList)) + val intent = SplitPdfResultActivity.createIntentPdfSelectedPagesItem( + this, + ArrayList(selectedList), + PdfPickerSource.SPLIT + ) startActivity(intent) finish() } @@ -151,21 +158,22 @@ class SplitPdfActivity : BaseActivity() { PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity) } // 删除完成后,开始收集数据 - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - var firstPageLoaded = false - PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem -> - if (splitList.size <= pageItem.pageIndex) { - splitList.add(pageItem) - adapter.notifyItemInserted(splitList.size - 1) - } else { - splitList[pageItem.pageIndex] = pageItem - adapter.updateItem(pageItem.pageIndex) - } - if (!firstPageLoaded) { - binding.loadingRoot.root.visibility = View.GONE + var firstPageLoaded = false + PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem -> + logDebug("splitPdfToPageItemsFlow pageItem->$pageItem") + if (splitList.size <= pageItem.pageIndex) { + splitList.add(pageItem) + adapter.notifyItemInserted(splitList.size - 1) + } else { + splitList[pageItem.pageIndex] = pageItem + adapter.updateItem(pageItem.pageIndex) + } + if (!firstPageLoaded) { + binding.loadingRoot.root.visibility = View.GONE + if (!isSelectedViewShow) { binding.selectAllBtn.visibility = View.VISIBLE - firstPageLoaded = true } + firstPageLoaded = true } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfResultActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfResultActivity.kt index 6da8d6b..1029958 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfResultActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfResultActivity.kt @@ -1,5 +1,6 @@ package com.all.pdfreader.pro.app.ui.act +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Build @@ -8,20 +9,17 @@ 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 +import com.all.pdfreader.pro.app.model.PdfPickerSource 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.FileUtils.toUnderscoreDateTime import com.all.pdfreader.pro.app.util.PdfScanner import com.all.pdfreader.pro.app.util.PdfUtils import com.gyf.immersionbar.ImmersionBar @@ -29,26 +27,41 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import java.io.Serializable class SplitPdfResultActivity : BaseActivity() { override val TAG: String = "SplitPdfResultActivity" companion object { private const val EXTRA_SELECTED_LIST = "extra_selected_list" + private const val EXTRA_FILE_LIST = "extra_file_list" + private const val EXTRA_SOURCE = "extra_source" - fun createIntent( - context: Context, list: ArrayList + fun createIntentPdfSelectedPagesItem( + context: Context, list: ArrayList, source: PdfPickerSource ): Intent { return Intent(context, SplitPdfResultActivity::class.java).apply { putParcelableArrayListExtra(EXTRA_SELECTED_LIST, list) + putExtra(EXTRA_SOURCE, source) + } + } + + fun createIntentInputFile( + context: Context, list: ArrayList, source: PdfPickerSource + ): Intent { + return Intent(context, SplitPdfResultActivity::class.java).apply { + putStringArrayListExtra(EXTRA_FILE_LIST, list) + putExtra(EXTRA_SOURCE, source) } } } private lateinit var binding: ActivityPdfSplitResultBinding private lateinit var adapter: SplitPdfResultAdapter - private var splitResultList: MutableList = mutableListOf() + private var resultList: MutableList = mutableListOf() private lateinit var selectedList: ArrayList + private lateinit var inputFile: ArrayList + private lateinit var source: PdfPickerSource private var isSplitting = false private var exitDialog: PromptDialogFragment? = null private val pdfRepository = getRepository() @@ -62,6 +75,27 @@ class SplitPdfResultActivity : BaseActivity() { ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) .navigationBarColor(R.color.bg_color).init() selectedList = requireParcelableArrayList(EXTRA_SELECTED_LIST) + inputFile = requireStringArrayList(EXTRA_FILE_LIST) + source = getSerializableOrDefault(EXTRA_SOURCE, PdfPickerSource.NONE) + if (source == PdfPickerSource.NONE) { + showToast(getString(R.string.pdf_loading_failed)) + finish() + return + } else { + if (source == PdfPickerSource.SPLIT) { + if (selectedList.isEmpty()) { + showToast(getString(R.string.pdf_loading_failed)) + finish() + return + } + } else if (source == PdfPickerSource.MERGE) { + if (inputFile.isEmpty()) { + showToast(getString(R.string.pdf_loading_failed)) + finish() + return + } + } + } pdfScanner = PdfScanner(this, pdfRepository) initView() setupClick() @@ -69,60 +103,88 @@ class SplitPdfResultActivity : BaseActivity() { } private fun initView() { - adapter = SplitPdfResultAdapter(splitResultList) + adapter = SplitPdfResultAdapter(resultList) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = adapter } 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.processingLayout.visibility = View.VISIBLE binding.progressBar.isIndeterminate = false binding.progressBar.progress = 0 binding.progressBar.max = 100 } + if (source == PdfPickerSource.SPLIT) { + val totalPages = selectedList.sumOf { it.pages.count { it.isSelected } } + var processedPages = 0 + 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() } - 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 = "${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 + PdfUtils.exportSelectedPages( + inputFile = inputFile, + selectedPages = selectedPages, + outputDir = outputDir, + 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) { + resultList.add(result) + } + } + } + } else if (source == PdfPickerSource.MERGE) { + if (inputFile.isNotEmpty()) { + val inputFiles: List = inputFile.map { path -> File(path) } + val outputDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), + "PDFReaderPro/merge" + ).apply { if (!exists()) mkdirs() } + val outputFileName = + getString(R.string.merge) + "_" + System.currentTimeMillis() + .toUnderscoreDateTime() + ".pdf" + PdfUtils.mergePdfFilesSafe( + inputFiles = inputFiles, + outputDir = outputDir, + outputFileName = outputFileName, + onProgress = { current, total -> + val progressPercent = current * 100 / total + lifecycleScope.launch(Dispatchers.Main) { + binding.progressBar.progress = progressPercent + binding.progressTv.text = "$progressPercent" + } + })?.let { resultFile -> + val thumbnails = + AppUtils.generateFastThumbnail(this@SplitPdfResultActivity, resultFile) + val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false) + pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) { + resultList.add(result) } - })?.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 + binding.processingLayout.visibility = View.GONE // 默认选中第一个 - if (splitResultList.isNotEmpty() && splitResultList.none { it.isSelected }) { - splitResultList[0].isSelected = true + if (resultList.isNotEmpty() && resultList.none { it.isSelected }) { + resultList[0].isSelected = true } adapter.updateAdapter() isSplitting = false//拆分结束 @@ -177,9 +239,6 @@ class SplitPdfResultActivity : BaseActivity() { }) } - /** - * 通用方法:读取必传参数,如果为 null 直接 finish - */ @Suppress("DEPRECATION") private inline fun requireParcelableArrayList(key: String): ArrayList { val result: ArrayList? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -187,11 +246,28 @@ class SplitPdfResultActivity : BaseActivity() { } else { intent.getParcelableArrayListExtra(key) } - - if (result.isNullOrEmpty()) { - showToast(getString(R.string.pdf_loading_failed)) - finish() - } return result ?: arrayListOf() } + + private fun requireStringArrayList(key: String): ArrayList { + val result: ArrayList? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getStringArrayListExtra(key) + } else { + @Suppress("DEPRECATION") intent.getStringArrayListExtra(key) + } + return result ?: arrayListOf() + } + + private inline fun getSerializableOrDefault( + key: String, default: T + ): T { + val result: T? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(key, T::class.java) + } else { + @Suppress("DEPRECATION") intent.getSerializableExtra(key) as? T + } + return result ?: default + } + } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt index acbdfee..a50de3b 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt @@ -17,12 +17,17 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners class PdfAdapter( private var pdfList: MutableList, - private val onItemClick: (PdfDocumentEntity) -> Unit, - private val onMoreClick: (PdfDocumentEntity) -> Unit, - private val onLongClick: (PdfDocumentEntity) -> Unit, - private val onSelectModelItemClick: (Int) -> Unit = {} + private val onItemClick: (PdfDocumentEntity) -> Unit = {}, + private val onMoreClick: (PdfDocumentEntity) -> Unit = {}, + private val onLongClick: (PdfDocumentEntity) -> Unit = {}, + private val onSelectModelItemClick: (Int) -> Unit = {}, + private val onDeleteItemClick: (PdfDocumentEntity, Int) -> Unit = { _, _ -> }, + private var enableLongClick: Boolean = true, + private var showMoreButton: Boolean = true, // 是否显示更多按钮 + private var showCheckButton: Boolean = false, // 是否显示选择框按钮 + private var isMultiSelectMode: Boolean = false, // 是否进入多选模式 + private var showDeleteButton: Boolean = false // 是否显示删除按钮 ) : RecyclerView.Adapter() { - private var isMultiSelectMode = false inner class PdfViewHolder(val binding: AdapterPdfItemBinding) : RecyclerView.ViewHolder(binding.root) @@ -57,13 +62,14 @@ class PdfAdapter( if (isMultiSelectMode) { holder.binding.checkBtn.visibility = View.VISIBLE holder.binding.moreBtn.visibility = View.GONE + holder.binding.deleteBtn.visibility = View.GONE } else { - holder.binding.checkBtn.visibility = View.GONE - holder.binding.moreBtn.visibility = View.VISIBLE + holder.binding.checkBtn.visibility = if (showCheckButton) View.VISIBLE else View.GONE + holder.binding.moreBtn.visibility = if (showMoreButton) View.VISIBLE else View.GONE + holder.binding.deleteBtn.visibility = if (showDeleteButton) View.VISIBLE else View.GONE } holder.binding.checkbox.isChecked = item.isSelected - holder.binding.root.setOnClickListener { if (isMultiSelectMode) { item.isSelected = !item.isSelected @@ -76,18 +82,27 @@ class PdfAdapter( holder.binding.moreBtn.setOnClickListener { onMoreClick(item) } - holder.binding.root.setOnLongClickListener { - if (!isMultiSelectMode) { - isMultiSelectMode = true - item.isSelected = !item.isSelected - notifyDataSetChanged() - onLongClick(item) + if (enableLongClick) { + holder.binding.root.setOnLongClickListener { + if (!isMultiSelectMode) { + isMultiSelectMode = true + item.isSelected = !item.isSelected + notifyDataSetChanged() + onLongClick(item) + } + true } - true + } else { + holder.binding.root.setOnLongClickListener(null) // 禁用长按 } + holder.binding.checkBtn.setOnClickListener { toggleSelection(item, holder.bindingAdapterPosition) } + + holder.binding.deleteBtn.setOnClickListener { + onDeleteItemClick(item, holder.bindingAdapterPosition) + } } override fun getItemCount(): Int = pdfList.size @@ -104,6 +119,11 @@ class PdfAdapter( notifyItemChanged(position) } + fun removeItem(position: Int) { + pdfList.removeAt(position) + notifyItemRemoved(position) + } + // 退出多选模式 @SuppressLint("NotifyDataSetChanged") fun exitMultiSelectMode() { @@ -138,7 +158,7 @@ class PdfAdapter( return pdfList.filter { it.isSelected } } - fun getIsMultiSelectMode(): Boolean{ + fun getIsMultiSelectMode(): Boolean { return isMultiSelectMode } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchPdfAdapter.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchPdfAdapter.kt index 4e7afe6..0217c44 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchPdfAdapter.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchPdfAdapter.kt @@ -20,7 +20,9 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners class SearchPdfAdapter( private val onItemClick: (PdfDocumentEntity) -> Unit, - private val onMoreClick: (PdfDocumentEntity) -> Unit + private val onMoreClick: (PdfDocumentEntity) -> Unit, + private var showMoreButton: Boolean = true, // 是否显示更多按钮 + private var showCheckButton: Boolean = false // 是否显示选择框按钮 ) : ListAdapter(PdfDiffCallback()) { inner class PdfViewHolder(val binding: AdapterPdfItemBinding) : @@ -50,6 +52,9 @@ class SearchPdfAdapter( private fun bindItem(holder: PdfViewHolder, item: PdfDocumentEntity, highlightKeyword: String?) { val context = holder.binding.root.context + holder.binding.checkBtn.visibility = if (showCheckButton) View.VISIBLE else View.GONE + holder.binding.moreBtn.visibility = if (showMoreButton) View.VISIBLE else View.GONE + // 文件名高亮,如果 highlightKeyword 为 null 或空字符串,就显示普通文本 holder.binding.tvFileName.text = if (highlightKeyword.isNullOrBlank()) { item.fileName diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt index 022c972..13113cf 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt @@ -84,15 +84,13 @@ class FavoriteFrag : BaseFrag() { private fun observeDocuments(onComplete: () -> Unit = {}) { lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - PdfRepository.getInstance().getFavoriteDocuments().collect { list -> - if (list.isNotEmpty()) { - adapter.updateData(list) - onComplete() - binding.noFilesLayout.visibility = View.GONE - } else { - binding.noFilesLayout.visibility = View.VISIBLE - } + PdfRepository.getInstance().getFavoriteDocuments().collect { list -> + if (list.isNotEmpty()) { + adapter.updateData(list) + onComplete() + binding.noFilesLayout.visibility = View.GONE + } else { + binding.noFilesLayout.visibility = View.VISIBLE } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt index 91ec484..c2a618c 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt @@ -93,17 +93,15 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment { private fun observeDocuments() { lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - PdfRepository.getInstance().getAllDocuments().collect { list -> - val sortedList = sortDocuments(list) - if (list.isNotEmpty()) { - adapter.updateData(sortedList) - binding.noFilesLayout.visibility = View.GONE - } else { - binding.noFilesLayout.visibility = View.VISIBLE - } - logDebug("更新adapter数据,排序方式: ${appStore.documentSortType}") + PdfRepository.getInstance().getAllDocuments().collect { list -> + val sortedList = sortDocuments(list) + if (list.isNotEmpty()) { + adapter.updateData(sortedList) + binding.noFilesLayout.visibility = View.GONE + } else { + binding.noFilesLayout.visibility = View.VISIBLE } + logDebug("更新adapter数据,排序方式: ${appStore.documentSortType}") } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt index f97999b..12fd8ad 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt @@ -83,15 +83,13 @@ class RecentlyFrag : BaseFrag() { private fun observeDocuments(onComplete: () -> Unit = {}) { lifecycleScope.launch { - viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - PdfRepository.getInstance().getRecentlyOpenedDocuments().collect { list -> - if (list.isNotEmpty()) { - adapter.updateData(list) - onComplete() - binding.noFilesLayout.visibility = View.GONE - } else { - binding.noFilesLayout.visibility = View.VISIBLE - } + PdfRepository.getInstance().getRecentlyOpenedDocuments().collect { list -> + if (list.isNotEmpty()) { + adapter.updateData(list) + onComplete() + binding.noFilesLayout.visibility = View.GONE + } else { + binding.noFilesLayout.visibility = View.VISIBLE } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt index a65fcbe..79cfdbd 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt @@ -6,6 +6,8 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding +import com.all.pdfreader.pro.app.model.PdfPickerSource +import com.all.pdfreader.pro.app.ui.act.PdfPickerActivity class ToolsFrag : Fragment() { private lateinit var binding: FragmentToolsBinding @@ -23,6 +25,13 @@ class ToolsFrag : Fragment() { } private fun initView() { - + binding.mergeBtn.setOnClickListener { + val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.MERGE) + startActivity(intent) + } + binding.splitBtn.setOnClickListener { + val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.SPLIT) + startActivity(intent) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt index c8e07ac..1c37798 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt @@ -7,6 +7,8 @@ import android.util.Log import androidx.core.graphics.createBitmap import com.all.pdfreader.pro.app.model.PdfPageItem import com.shockwave.pdfium.PdfiumCore +import com.tom_roush.pdfbox.io.MemoryUsageSetting +import com.tom_roush.pdfbox.multipdf.PDFMergerUtility import com.tom_roush.pdfbox.pdmodel.PDDocument import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -138,8 +140,7 @@ object PdfUtils { /** * 合并多个 PDF 文件到一个新的 PDF 文件 * - * 使用 PDFBox 的 PDDocument 合并多个 PDF,支持进度回调。 - * 避免直接生成缩略图,以保持原 PDF 的矢量质量。 + * 使用 PDFBox 的 PDFMergerUtility 合并 PDF,支持进度回调。 * * @param inputFiles 要合并的 PDF 文件列表 * @param outputDir 输出目录,如果不存在会自动创建 @@ -147,7 +148,18 @@ object PdfUtils { * @param onProgress 可选回调,当前处理进度 (current 文件, total 文件) * @return 新生成的 PDF 文件,失败返回 null */ - suspend fun mergePdfFiles( + /** + * 合并多个 PDF 文件到一个新的 PDF 文件 + * + * 使用 PDFBox 的 PDFMergerUtility 合并 PDF,支持进度回调。 + * + * @param inputFiles 要合并的 PDF 文件列表 + * @param outputDir 输出目录,如果不存在会自动创建 + * @param outputFileName 输出文件名,例如 "merged.pdf" + * @param onProgress 可选回调,当前处理进度 (current 文件, total 文件) + * @return 新生成的 PDF 文件,失败返回 null + */ + suspend fun mergePdfFilesSafe( inputFiles: List, outputDir: File, outputFileName: String, @@ -156,28 +168,25 @@ object PdfUtils { if (inputFiles.isEmpty()) return@withContext null if (!outputDir.exists()) outputDir.mkdirs() + val outputFile = File(outputDir, outputFileName) try { - PDDocument().use { mergedDocument -> - val totalFiles = inputFiles.size - inputFiles.forEachIndexed { index, file -> - PDDocument.load(file).use { doc -> - for (page in doc.pages) { - mergedDocument.addPage(page) - } - } - // 回调进度:按文件计数,也可以按总页数进一步精细化 - onProgress?.invoke(index + 1, totalFiles) - delay(1) // 给 UI 更新留点时间 - } - mergedDocument.save(outputFile) + val merger = PDFMergerUtility() + inputFiles.forEachIndexed { index, file -> + merger.addSource(file) + onProgress?.invoke(index + 1, inputFiles.size) + delay(100) } + + merger.destinationFileName = outputFile.absolutePath + + merger.mergeDocuments(MemoryUsageSetting.setupTempFileOnly()) + outputFile } catch (e: Exception) { e.printStackTrace() null } } - } diff --git a/app/src/main/res/drawable/dr_cancel_btn_bg.xml b/app/src/main/res/drawable/dr_cancel_btn_bg.xml index 59149af..e1a41a5 100644 --- a/app/src/main/res/drawable/dr_cancel_btn_bg.xml +++ b/app/src/main/res/drawable/dr_cancel_btn_bg.xml @@ -6,7 +6,7 @@ - + diff --git a/app/src/main/res/drawable/dr_click_btn_bg.xml b/app/src/main/res/drawable/dr_click_btn_bg.xml index c56f589..ae6cbd9 100644 --- a/app/src/main/res/drawable/dr_click_btn_bg.xml +++ b/app/src/main/res/drawable/dr_click_btn_bg.xml @@ -6,7 +6,7 @@ - + diff --git a/app/src/main/res/drawable/dr_item_img_frame.xml b/app/src/main/res/drawable/dr_item_img_frame.xml index cb2fb17..63d6b9f 100644 --- a/app/src/main/res/drawable/dr_item_img_frame.xml +++ b/app/src/main/res/drawable/dr_item_img_frame.xml @@ -2,6 +2,6 @@ + android:width="0.5dp"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pdf_merge.xml b/app/src/main/res/layout/activity_pdf_merge.xml new file mode 100644 index 0000000..16aa98f --- /dev/null +++ b/app/src/main/res/layout/activity_pdf_merge.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_pdf_picker.xml b/app/src/main/res/layout/activity_pdf_picker.xml new file mode 100644 index 0000000..b6513bc --- /dev/null +++ b/app/src/main/res/layout/activity_pdf_picker.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_pdf_split_result.xml b/app/src/main/res/layout/activity_pdf_split_result.xml index 94a7a52..b083ca5 100644 --- a/app/src/main/res/layout/activity_pdf_split_result.xml +++ b/app/src/main/res/layout/activity_pdf_split_result.xml @@ -12,7 +12,7 @@ android:layout_height="0dp" /> diff --git a/app/src/main/res/layout/adapter_merge_list_item.xml b/app/src/main/res/layout/adapter_merge_list_item.xml new file mode 100644 index 0000000..c4b6a0a --- /dev/null +++ b/app/src/main/res/layout/adapter_merge_list_item.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/adapter_pdf_item.xml b/app/src/main/res/layout/adapter_pdf_item.xml index 386a925..bc4eafd 100644 --- a/app/src/main/res/layout/adapter_pdf_item.xml +++ b/app/src/main/res/layout/adapter_pdf_item.xml @@ -22,8 +22,7 @@ + android:layout_height="56dp"> - + + + + @@ -130,6 +137,7 @@ android:layout_height="24dp" android:src="@drawable/more" /> + + + + + + diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml index d287546..b6d6bc8 100644 --- a/app/src/main/res/layout/fragment_tools.xml +++ b/app/src/main/res/layout/fragment_tools.xml @@ -9,9 +9,17 @@ android:layout_width="match_parent" android:layout_height="0dp" /> - + android:text="@string/merge_pdf" /> + +