添加合并pdf功能

This commit is contained in:
ocean 2025-09-30 15:15:40 +08:00
parent e20a8ab016
commit c04dae1fe3
26 changed files with 1106 additions and 157 deletions

1
.idea/misc.xml generated
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CMakeSettings"> <component name="CMakeSettings">
<configurations> <configurations>

View File

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

View File

@ -0,0 +1,10 @@
package com.all.pdfreader.pro.app.model
enum class PdfPickerSource {
NONE, // 没有任何
MERGE, // PDF合并
SPLIT, // PDF拆分
LOCK, // PDF加密/解密
TO_IMAGES, // PDF转图片
TO_LONG_IMAGE // PDF转长图
}

View File

@ -227,16 +227,17 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
} }
} }
binding.multiSelectMergeBtn.setOnSingleClickListener { binding.multiSelectMergeBtn.setOnSingleClickListener {
logDebug("合并") val selectedItems = when (activeFragment) {
val selectedItems = recentlyFragment.adapter.getSelectedItems() is HomeFrag -> (activeFragment as HomeFrag).adapter.getSelectedItems()
// if (selectedItems.isNotEmpty()) { is FavoriteFrag -> (activeFragment as FavoriteFrag).adapter.getSelectedItems()
// val inputFile = selectedItems.map { File(it.filePath) } is RecentlyFrag -> (activeFragment as RecentlyFrag).adapter.getSelectedItems()
// val outputDir = File( else -> emptyList()
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), }
// "PDFReaderPro/merge" if (selectedItems.isNotEmpty()) {
// ).apply { if (!exists()) mkdirs() } val intent = MergePdfActivity.createIntent(this, ArrayList(selectedItems))
// PdfUtils.mergePdfFiles(inputFiles = inputFile, outputDir =outputDir, onProgress = {} ) startActivity(intent)
// } exitAllMultiSelect()
}
} }
binding.multiSelectRemoveBtn.setOnSingleClickListener { binding.multiSelectRemoveBtn.setOnSingleClickListener {
val selectedItems = recentlyFragment.adapter.getSelectedItems() val selectedItems = recentlyFragment.adapter.getSelectedItems()

View File

@ -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<PdfDocumentEntity>
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<PdfDocumentEntity>) {
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<PdfDocumentEntity>): Intent {
return Intent(context, MergePdfActivity::class.java).apply {
putParcelableArrayListExtra(EXTRA_PDF_LIST, list)
}
}
}
/**
* 通用方法读取必传参数如果为 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()
}
class PickPdfContract(
private val from: String
) : ActivityResultContract<Pair<PdfPickerSource, ArrayList<PdfDocumentEntity>>, List<PdfDocumentEntity>>() {
override fun createIntent(
context: Context, input: Pair<PdfPickerSource, ArrayList<PdfDocumentEntity>>
): Intent {
val (source, historyList) = input
return PdfPickerActivity.createIntent(context, source, from, historyList)
}
override fun parseResult(resultCode: Int, intent: Intent?): List<PdfDocumentEntity> {
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<PdfDocumentEntity>(
EXTRA_SELECTED_LIST
) ?: emptyList()
}
}
}
}

View File

@ -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<PdfDocumentEntity>
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<PdfDocumentEntity>, historyList: List<PdfDocumentEntity>
) {
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<PdfDocumentEntity>) {
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<PdfDocumentEntity>): List<PdfDocumentEntity> {
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<PdfDocumentEntity> = 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 <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)
}
return result ?: arrayListOf()
}
private inline fun <reified T : Serializable> 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
}
}

View File

@ -215,14 +215,13 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
override fun onError(t: Throwable?) { override fun onError(t: Throwable?) {
logDebug("PDF loading error: ${t?.message}") logDebug("PDF loading error: ${t?.message}")
t?.let { t?.let {
val errorMessage = it.message ?: "未知错误" val errorMessage = it.message ?: getString(R.string.pdf_loading_failed)
// 检查是否是密码相关的错误 // 检查是否是密码相关的错误
if (errorMessage.contains("Password") || errorMessage.contains("password")) { if (errorMessage.contains("Password") || errorMessage.contains("password")) {
val file = File(pdfDocument.filePath) val file = File(pdfDocument.filePath)
showPasswordDialog(file) showPasswordDialog(file)
} else { } else {
// 其他错误 showToast(errorMessage)
showToast(getString(R.string.pdf_loading_failed))
finish() finish()
} }
} }

View File

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitBinding
import com.all.pdfreader.pro.app.model.PdfPageItem 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.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.RenameType import com.all.pdfreader.pro.app.model.RenameType
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
@ -121,7 +122,8 @@ class SplitPdfActivity : BaseActivity() {
} }
binding.continueNowBtn.setOnSingleClickListener { binding.continueNowBtn.setOnSingleClickListener {
val selectedPages = splitList.filter { it.isSelected }.map { it.copy() } 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) val item = PdfSelectedPagesItem(filePath, name, selectedPages)
selectedList.add(item) selectedList.add(item)
selectedPdfAdapter.updateAdapter() selectedPdfAdapter.updateAdapter()
@ -136,8 +138,13 @@ class SplitPdfActivity : BaseActivity() {
updateViewState(false) updateViewState(false)
} }
binding.splitBtn.setOnSingleClickListener { binding.splitBtn.setOnSingleClickListener {
logDebug("${selectedList.size}")
//因为图片做的路径缓存方式所以这里直接传入整个集合到result页处理 //因为图片做的路径缓存方式所以这里直接传入整个集合到result页处理
val intent = SplitPdfResultActivity.createIntent(this, ArrayList(selectedList)) val intent = SplitPdfResultActivity.createIntentPdfSelectedPagesItem(
this,
ArrayList(selectedList),
PdfPickerSource.SPLIT
)
startActivity(intent) startActivity(intent)
finish() finish()
} }
@ -151,21 +158,22 @@ class SplitPdfActivity : BaseActivity() {
PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity) PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity)
} }
// 删除完成后,开始收集数据 // 删除完成后,开始收集数据
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { var firstPageLoaded = false
var firstPageLoaded = false PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem ->
PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem -> logDebug("splitPdfToPageItemsFlow pageItem->$pageItem")
if (splitList.size <= pageItem.pageIndex) { if (splitList.size <= pageItem.pageIndex) {
splitList.add(pageItem) splitList.add(pageItem)
adapter.notifyItemInserted(splitList.size - 1) adapter.notifyItemInserted(splitList.size - 1)
} else { } else {
splitList[pageItem.pageIndex] = pageItem splitList[pageItem.pageIndex] = pageItem
adapter.updateItem(pageItem.pageIndex) adapter.updateItem(pageItem.pageIndex)
} }
if (!firstPageLoaded) { if (!firstPageLoaded) {
binding.loadingRoot.root.visibility = View.GONE binding.loadingRoot.root.visibility = View.GONE
if (!isSelectedViewShow) {
binding.selectAllBtn.visibility = View.VISIBLE binding.selectAllBtn.visibility = View.VISIBLE
firstPageLoaded = true
} }
firstPageLoaded = true
} }
} }
} }

View File

@ -1,5 +1,6 @@
package com.all.pdfreader.pro.app.ui.act package com.all.pdfreader.pro.app.ui.act
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@ -8,20 +9,17 @@ import android.os.Environment
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager 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.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding 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.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.PdfSplitResultItem 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.adapter.SplitPdfResultAdapter
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils 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.PdfScanner
import com.all.pdfreader.pro.app.util.PdfUtils import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
@ -29,26 +27,41 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.Serializable
class SplitPdfResultActivity : BaseActivity() { class SplitPdfResultActivity : BaseActivity() {
override val TAG: String = "SplitPdfResultActivity" override val TAG: String = "SplitPdfResultActivity"
companion object { companion object {
private const val EXTRA_SELECTED_LIST = "extra_selected_list" 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( fun createIntentPdfSelectedPagesItem(
context: Context, list: ArrayList<PdfSelectedPagesItem> context: Context, list: ArrayList<PdfSelectedPagesItem>, source: PdfPickerSource
): Intent { ): Intent {
return Intent(context, SplitPdfResultActivity::class.java).apply { return Intent(context, SplitPdfResultActivity::class.java).apply {
putParcelableArrayListExtra(EXTRA_SELECTED_LIST, list) putParcelableArrayListExtra(EXTRA_SELECTED_LIST, list)
putExtra(EXTRA_SOURCE, source)
}
}
fun createIntentInputFile(
context: Context, list: ArrayList<String>, 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 binding: ActivityPdfSplitResultBinding
private lateinit var adapter: SplitPdfResultAdapter private lateinit var adapter: SplitPdfResultAdapter
private var splitResultList: MutableList<PdfSplitResultItem> = mutableListOf() private var resultList: MutableList<PdfSplitResultItem> = mutableListOf()
private lateinit var selectedList: ArrayList<PdfSelectedPagesItem> private lateinit var selectedList: ArrayList<PdfSelectedPagesItem>
private lateinit var inputFile: ArrayList<String>
private lateinit var source: PdfPickerSource
private var isSplitting = false private var isSplitting = false
private var exitDialog: PromptDialogFragment? = null private var exitDialog: PromptDialogFragment? = null
private val pdfRepository = getRepository() private val pdfRepository = getRepository()
@ -62,6 +75,27 @@ class SplitPdfResultActivity : BaseActivity() {
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init() .navigationBarColor(R.color.bg_color).init()
selectedList = requireParcelableArrayList(EXTRA_SELECTED_LIST) 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) pdfScanner = PdfScanner(this, pdfRepository)
initView() initView()
setupClick() setupClick()
@ -69,60 +103,88 @@ class SplitPdfResultActivity : BaseActivity() {
} }
private fun initView() { private fun initView() {
adapter = SplitPdfResultAdapter(splitResultList) adapter = SplitPdfResultAdapter(resultList)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
} }
private fun initData() { private fun initData() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val totalPages = selectedList.sumOf { it.pages.count { it.isSelected } }
var processedPages = 0
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isSplitting = true isSplitting = true
binding.splittingLayout.visibility = View.VISIBLE binding.processingLayout.visibility = View.VISIBLE
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0 binding.progressBar.progress = 0
binding.progressBar.max = 100 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) { PdfUtils.exportSelectedPages(
val selectedPages = item.pages.filter { it.isSelected } inputFile = inputFile,
if (selectedPages.isEmpty()) continue selectedPages = selectedPages,
outputDir = outputDir,
val inputFile = File(item.filePath) outputFileName = "${item.fileName}.pdf",
val outputDir = File( onProgress = { _, _ -> // 不需要单文件百分比
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), processedPages++// 每页处理完成就加一,多个 PDF 顺序处理时,总进度线性递增
"PDFReaderPro/split" val percent = (processedPages.toFloat() / totalPages * 100).toInt()
).apply { if (!exists()) mkdirs() } lifecycleScope.launch(Dispatchers.Main) {
binding.progressTv.text = "$percent"
PdfUtils.exportSelectedPages( binding.progressBar.progress = percent
inputFile = inputFile, }
selectedPages = selectedPages, })?.let { resultFile ->
outputDir = outputDir, val thumbnails =
outputFileName = "${item.fileName}.pdf", AppUtils.generateFastThumbnail(this@SplitPdfResultActivity, resultFile)
onProgress = { _, _ -> // 不需要单文件百分比 val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
processedPages++// 每页处理完成就加一,多个 PDF 顺序处理时,总进度线性递增 pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) {
val percent = (processedPages.toFloat() / totalPages * 100).toInt() resultList.add(result)
lifecycleScope.launch(Dispatchers.Main) { }
binding.progressTv.text = "$percent" }
binding.progressBar.progress = percent }
} else if (source == PdfPickerSource.MERGE) {
if (inputFile.isNotEmpty()) {
val inputFiles: List<File> = 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) { withContext(Dispatchers.Main) {
binding.splittingLayout.visibility = View.GONE binding.processingLayout.visibility = View.GONE
// 默认选中第一个 // 默认选中第一个
if (splitResultList.isNotEmpty() && splitResultList.none { it.isSelected }) { if (resultList.isNotEmpty() && resultList.none { it.isSelected }) {
splitResultList[0].isSelected = true resultList[0].isSelected = true
} }
adapter.updateAdapter() adapter.updateAdapter()
isSplitting = false//拆分结束 isSplitting = false//拆分结束
@ -177,9 +239,6 @@ class SplitPdfResultActivity : BaseActivity() {
}) })
} }
/**
* 通用方法读取必传参数如果为 null 直接 finish
*/
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private inline fun <reified T : Parcelable> requireParcelableArrayList(key: String): ArrayList<T> { private inline fun <reified T : Parcelable> requireParcelableArrayList(key: String): ArrayList<T> {
val result: ArrayList<T>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val result: ArrayList<T>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -187,11 +246,28 @@ class SplitPdfResultActivity : BaseActivity() {
} else { } else {
intent.getParcelableArrayListExtra(key) intent.getParcelableArrayListExtra(key)
} }
if (result.isNullOrEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
}
return result ?: arrayListOf() return result ?: arrayListOf()
} }
private fun requireStringArrayList(key: String): ArrayList<String> {
val result: ArrayList<String>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getStringArrayListExtra(key)
} else {
@Suppress("DEPRECATION") intent.getStringArrayListExtra(key)
}
return result ?: arrayListOf()
}
private inline fun <reified T : Serializable> 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
}
} }

View File

@ -17,12 +17,17 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
class PdfAdapter( class PdfAdapter(
private var pdfList: MutableList<PdfDocumentEntity>, private var pdfList: MutableList<PdfDocumentEntity>,
private val onItemClick: (PdfDocumentEntity) -> Unit, private val onItemClick: (PdfDocumentEntity) -> Unit = {},
private val onMoreClick: (PdfDocumentEntity) -> Unit, private val onMoreClick: (PdfDocumentEntity) -> Unit = {},
private val onLongClick: (PdfDocumentEntity) -> Unit, private val onLongClick: (PdfDocumentEntity) -> Unit = {},
private val onSelectModelItemClick: (Int) -> 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<PdfAdapter.PdfViewHolder>() { ) : RecyclerView.Adapter<PdfAdapter.PdfViewHolder>() {
private var isMultiSelectMode = false
inner class PdfViewHolder(val binding: AdapterPdfItemBinding) : inner class PdfViewHolder(val binding: AdapterPdfItemBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
@ -57,13 +62,14 @@ class PdfAdapter(
if (isMultiSelectMode) { if (isMultiSelectMode) {
holder.binding.checkBtn.visibility = View.VISIBLE holder.binding.checkBtn.visibility = View.VISIBLE
holder.binding.moreBtn.visibility = View.GONE holder.binding.moreBtn.visibility = View.GONE
holder.binding.deleteBtn.visibility = View.GONE
} else { } else {
holder.binding.checkBtn.visibility = View.GONE holder.binding.checkBtn.visibility = if (showCheckButton) View.VISIBLE else View.GONE
holder.binding.moreBtn.visibility = View.VISIBLE 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.checkbox.isChecked = item.isSelected
holder.binding.root.setOnClickListener { holder.binding.root.setOnClickListener {
if (isMultiSelectMode) { if (isMultiSelectMode) {
item.isSelected = !item.isSelected item.isSelected = !item.isSelected
@ -76,18 +82,27 @@ class PdfAdapter(
holder.binding.moreBtn.setOnClickListener { holder.binding.moreBtn.setOnClickListener {
onMoreClick(item) onMoreClick(item)
} }
holder.binding.root.setOnLongClickListener { if (enableLongClick) {
if (!isMultiSelectMode) { holder.binding.root.setOnLongClickListener {
isMultiSelectMode = true if (!isMultiSelectMode) {
item.isSelected = !item.isSelected isMultiSelectMode = true
notifyDataSetChanged() item.isSelected = !item.isSelected
onLongClick(item) notifyDataSetChanged()
onLongClick(item)
}
true
} }
true } else {
holder.binding.root.setOnLongClickListener(null) // 禁用长按
} }
holder.binding.checkBtn.setOnClickListener { holder.binding.checkBtn.setOnClickListener {
toggleSelection(item, holder.bindingAdapterPosition) toggleSelection(item, holder.bindingAdapterPosition)
} }
holder.binding.deleteBtn.setOnClickListener {
onDeleteItemClick(item, holder.bindingAdapterPosition)
}
} }
override fun getItemCount(): Int = pdfList.size override fun getItemCount(): Int = pdfList.size
@ -104,6 +119,11 @@ class PdfAdapter(
notifyItemChanged(position) notifyItemChanged(position)
} }
fun removeItem(position: Int) {
pdfList.removeAt(position)
notifyItemRemoved(position)
}
// 退出多选模式 // 退出多选模式
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun exitMultiSelectMode() { fun exitMultiSelectMode() {
@ -138,7 +158,7 @@ class PdfAdapter(
return pdfList.filter { it.isSelected } return pdfList.filter { it.isSelected }
} }
fun getIsMultiSelectMode(): Boolean{ fun getIsMultiSelectMode(): Boolean {
return isMultiSelectMode return isMultiSelectMode
} }
} }

View File

@ -20,7 +20,9 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
class SearchPdfAdapter( class SearchPdfAdapter(
private val onItemClick: (PdfDocumentEntity) -> Unit, 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<PdfDocumentEntity, SearchPdfAdapter.PdfViewHolder>(PdfDiffCallback()) { ) : ListAdapter<PdfDocumentEntity, SearchPdfAdapter.PdfViewHolder>(PdfDiffCallback()) {
inner class PdfViewHolder(val binding: AdapterPdfItemBinding) : inner class PdfViewHolder(val binding: AdapterPdfItemBinding) :
@ -50,6 +52,9 @@ class SearchPdfAdapter(
private fun bindItem(holder: PdfViewHolder, item: PdfDocumentEntity, highlightKeyword: String?) { private fun bindItem(holder: PdfViewHolder, item: PdfDocumentEntity, highlightKeyword: String?) {
val context = holder.binding.root.context 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 或空字符串,就显示普通文本 // 文件名高亮,如果 highlightKeyword 为 null 或空字符串,就显示普通文本
holder.binding.tvFileName.text = if (highlightKeyword.isNullOrBlank()) { holder.binding.tvFileName.text = if (highlightKeyword.isNullOrBlank()) {
item.fileName item.fileName

View File

@ -84,15 +84,13 @@ class FavoriteFrag : BaseFrag() {
private fun observeDocuments(onComplete: () -> Unit = {}) { private fun observeDocuments(onComplete: () -> Unit = {}) {
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { PdfRepository.getInstance().getFavoriteDocuments().collect { list ->
PdfRepository.getInstance().getFavoriteDocuments().collect { list -> if (list.isNotEmpty()) {
if (list.isNotEmpty()) { adapter.updateData(list)
adapter.updateData(list) onComplete()
onComplete() binding.noFilesLayout.visibility = View.GONE
binding.noFilesLayout.visibility = View.GONE } else {
} else { binding.noFilesLayout.visibility = View.VISIBLE
binding.noFilesLayout.visibility = View.VISIBLE
}
} }
} }
} }

View File

@ -93,17 +93,15 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
private fun observeDocuments() { private fun observeDocuments() {
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { PdfRepository.getInstance().getAllDocuments().collect { list ->
PdfRepository.getInstance().getAllDocuments().collect { list -> val sortedList = sortDocuments(list)
val sortedList = sortDocuments(list) if (list.isNotEmpty()) {
if (list.isNotEmpty()) { adapter.updateData(sortedList)
adapter.updateData(sortedList) binding.noFilesLayout.visibility = View.GONE
binding.noFilesLayout.visibility = View.GONE } else {
} else { binding.noFilesLayout.visibility = View.VISIBLE
binding.noFilesLayout.visibility = View.VISIBLE
}
logDebug("更新adapter数据排序方式: ${appStore.documentSortType}")
} }
logDebug("更新adapter数据排序方式: ${appStore.documentSortType}")
} }
} }
} }

View File

@ -83,15 +83,13 @@ class RecentlyFrag : BaseFrag() {
private fun observeDocuments(onComplete: () -> Unit = {}) { private fun observeDocuments(onComplete: () -> Unit = {}) {
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { PdfRepository.getInstance().getRecentlyOpenedDocuments().collect { list ->
PdfRepository.getInstance().getRecentlyOpenedDocuments().collect { list -> if (list.isNotEmpty()) {
if (list.isNotEmpty()) { adapter.updateData(list)
adapter.updateData(list) onComplete()
onComplete() binding.noFilesLayout.visibility = View.GONE
binding.noFilesLayout.visibility = View.GONE } else {
} else { binding.noFilesLayout.visibility = View.VISIBLE
binding.noFilesLayout.visibility = View.VISIBLE
}
} }
} }
} }

View File

@ -6,6 +6,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding 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() { class ToolsFrag : Fragment() {
private lateinit var binding: FragmentToolsBinding private lateinit var binding: FragmentToolsBinding
@ -23,6 +25,13 @@ class ToolsFrag : Fragment() {
} }
private fun initView() { 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)
}
} }
} }

View File

@ -7,6 +7,8 @@ import android.util.Log
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import com.all.pdfreader.pro.app.model.PdfPageItem import com.all.pdfreader.pro.app.model.PdfPageItem
import com.shockwave.pdfium.PdfiumCore 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 com.tom_roush.pdfbox.pdmodel.PDDocument
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -138,8 +140,7 @@ object PdfUtils {
/** /**
* 合并多个 PDF 文件到一个新的 PDF 文件 * 合并多个 PDF 文件到一个新的 PDF 文件
* *
* 使用 PDFBox PDDocument 合并多个 PDF支持进度回调 * 使用 PDFBox PDFMergerUtility 合并 PDF支持进度回调
* 避免直接生成缩略图以保持原 PDF 的矢量质量
* *
* @param inputFiles 要合并的 PDF 文件列表 * @param inputFiles 要合并的 PDF 文件列表
* @param outputDir 输出目录如果不存在会自动创建 * @param outputDir 输出目录如果不存在会自动创建
@ -147,7 +148,18 @@ object PdfUtils {
* @param onProgress 可选回调当前处理进度 (current 文件, total 文件) * @param onProgress 可选回调当前处理进度 (current 文件, total 文件)
* @return 新生成的 PDF 文件失败返回 null * @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<File>, inputFiles: List<File>,
outputDir: File, outputDir: File,
outputFileName: String, outputFileName: String,
@ -156,28 +168,25 @@ object PdfUtils {
if (inputFiles.isEmpty()) return@withContext null if (inputFiles.isEmpty()) return@withContext null
if (!outputDir.exists()) outputDir.mkdirs() if (!outputDir.exists()) outputDir.mkdirs()
val outputFile = File(outputDir, outputFileName) val outputFile = File(outputDir, outputFileName)
try { try {
PDDocument().use { mergedDocument -> val merger = PDFMergerUtility()
val totalFiles = inputFiles.size inputFiles.forEachIndexed { index, file ->
inputFiles.forEachIndexed { index, file -> merger.addSource(file)
PDDocument.load(file).use { doc -> onProgress?.invoke(index + 1, inputFiles.size)
for (page in doc.pages) { delay(100)
mergedDocument.addPage(page)
}
}
// 回调进度:按文件计数,也可以按总页数进一步精细化
onProgress?.invoke(index + 1, totalFiles)
delay(1) // 给 UI 更新留点时间
}
mergedDocument.save(outputFile)
} }
merger.destinationFileName = outputFile.absolutePath
merger.mergeDocuments(MemoryUsageSetting.setupTempFileOnly())
outputFile outputFile
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }
} }
} }

View File

@ -6,7 +6,7 @@
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/grey" /> <solid android:color="@color/grey" />
<corners android:radius="12dp" /> <corners android:radius="24dp" />
</shape> </shape>
</item> </item>

View File

@ -6,7 +6,7 @@
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#E43521"/> <solid android:color="#E43521"/>
<corners android:radius="12dp"/> <corners android:radius="24dp"/>
</shape> </shape>
</item> </item>

View File

@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<stroke android:color="@color/line_color" <stroke android:color="@color/line_color"
android:width="1dp"/> android:width="0.5dp"/>
<corners android:radius="8dp" /> <corners android:radius="8dp" />
</shape> </shape>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/backBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/back_black" />
</LinearLayout>
<TextView
android:id="@+id/title"
style="@style/TextViewFont_PopMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/merge_pdf"
android:textColor="@color/black"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/addBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView
android:id="@+id/addIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/dr_circular_sel_on_bg"
android:padding="2dp"
android:src="@drawable/add_icon_white" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/noFilesLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/img_no_files_yet" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_files_yet"
android:textColor="#B6BFCC"
android:textSize="20sp" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/continue_now_btn"
android:overScrollMode="never"
android:scrollbars="none" />
<LinearLayout
android:id="@+id/continue_now_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/dr_btn_not_clickable_bg"
android:gravity="center">
<TextView
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/continue_now"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/backBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/back_black" />
</LinearLayout>
<TextView
android:id="@+id/title"
style="@style/TextViewFont_PopMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/searchBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/search" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/noFilesLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/img_no_files_yet" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_files_yet"
android:textColor="#B6BFCC"
android:textSize="20sp" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/continue_now_btn"
android:overScrollMode="never"
android:scrollbars="none" />
<LinearLayout
android:id="@+id/continue_now_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/dr_btn_not_clickable_bg"
android:gravity="center">
<TextView
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/continue_now"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@ -12,7 +12,7 @@
android:layout_height="0dp" /> android:layout_height="0dp" />
<LinearLayout <LinearLayout
android:id="@+id/splittingLayout" android:id="@+id/processingLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
@ -48,7 +48,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/splitting" android:text="@string/processing"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="18sp" /> android:textSize="18sp" />

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/dr_item_img_frame">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/dr_placeholder_bg"
android:gravity="center">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/file_document" />
</LinearLayout>
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="56dp"
android:layout_height="56dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/editTitleBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:id="@+id/tvFileName"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/icon_color"
android:textSize="16sp" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/edit_icon" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="2dp"
android:background="@drawable/underline_dashed" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/pages"
android:textColor="@color/black_60"
android:textSize="14sp" />
<TextView
android:id="@+id/tvPages"
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="0"
android:textColor="@color/black_60"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/deleteBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/delete_cha_icon" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@color/line_color" />
</LinearLayout>

View File

@ -22,8 +22,7 @@
<RelativeLayout <RelativeLayout
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp">
android:background="@drawable/dr_item_img_frame">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -38,10 +37,17 @@
</LinearLayout> </LinearLayout>
<ImageView <RelativeLayout
android:id="@+id/tvFileImg" android:layout_width="wrap_content"
android:layout_width="56dp" android:layout_height="wrap_content"
android:layout_height="56dp" /> android:background="@drawable/dr_item_img_frame">
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_margin="0.5dp" />
</RelativeLayout>
<ImageView <ImageView
android:id="@+id/collectState" android:id="@+id/collectState"
@ -73,6 +79,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
@ -130,6 +137,7 @@
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/more" /> android:src="@drawable/more" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/checkBtn" android:id="@+id/checkBtn"
android:layout_width="48dp" android:layout_width="48dp"
@ -147,6 +155,19 @@
android:focusable="false" /> android:focusable="false" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/deleteBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/delete_cha_icon" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -9,9 +9,17 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" /> android:layout_height="0dp" />
<TextView
android:layout_width="wrap_content" <Button
android:id="@+id/mergeBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/tools" /> android:text="@string/merge_pdf" />
<Button
android:id="@+id/splitBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/split_pdf" />
</LinearLayout> </LinearLayout>

View File

@ -150,4 +150,6 @@
<string name="tip">Tip</string> <string name="tip">Tip</string>
<string name="clear_history_desc">Are you sure you want to clear history?</string> <string name="clear_history_desc">Are you sure you want to clear history?</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="please_select_a_file">Please select a file</string>
<string name="unknown_source">Unknown Source</string>
</resources> </resources>