1.再次优化扫描文件性能,异步获取缩略图

2.添加列表item,更多设置选项对话框
3.修复数据库键值,不使用hash来作为键值,依据文件路径来判定
4.修复密码输入框实现,使用自带的错误等提示TextInputEditText
This commit is contained in:
ocean 2025-09-10 11:11:11 +08:00
parent ff14a8f3ee
commit 369dc9d129
40 changed files with 1048 additions and 315 deletions

View File

@ -19,18 +19,18 @@ interface BookmarkDao {
@Query("SELECT * FROM bookmarks WHERE id = :bookmarkId") @Query("SELECT * FROM bookmarks WHERE id = :bookmarkId")
suspend fun getById(bookmarkId: Long): BookmarkEntity? suspend fun getById(bookmarkId: Long): BookmarkEntity?
@Query("SELECT * FROM bookmarks WHERE pdfHash = :pdfHash ORDER BY pageNumber ASC, createTime ASC") @Query("SELECT * FROM bookmarks WHERE filePath = :filePath ORDER BY pageNumber ASC, createTime ASC")
fun getBookmarksByPdf(pdfHash: String): Flow<List<BookmarkEntity>> fun getBookmarksByPdf(filePath: String): Flow<List<BookmarkEntity>>
@Query("SELECT * FROM bookmarks WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber") @Query("SELECT * FROM bookmarks WHERE filePath = :filePath AND pageNumber = :pageNumber")
suspend fun getBookmarksByPage(pdfHash: String, pageNumber: Int): List<BookmarkEntity> suspend fun getBookmarksByPage(filePath: String, pageNumber: Int): List<BookmarkEntity>
@Query("SELECT COUNT(*) FROM bookmarks WHERE pdfHash = :pdfHash") @Query("SELECT COUNT(*) FROM bookmarks WHERE filePath = :filePath")
suspend fun getBookmarkCount(pdfHash: String): Int suspend fun getBookmarkCount(filePath: String): Int
@Query("DELETE FROM bookmarks WHERE pdfHash = :pdfHash") @Query("DELETE FROM bookmarks WHERE filePath = :filePath")
suspend fun deleteAllByPdf(pdfHash: String) suspend fun deleteAllByPdf(filePath: String)
@Query("DELETE FROM bookmarks WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber") @Query("DELETE FROM bookmarks WHERE filePath = :filePath AND pageNumber = :pageNumber")
suspend fun deleteByPage(pdfHash: String, pageNumber: Int) suspend fun deleteByPage(filePath: String, pageNumber: Int)
} }

View File

@ -19,21 +19,21 @@ interface NoteDao {
@Query("SELECT * FROM notes WHERE id = :noteId") @Query("SELECT * FROM notes WHERE id = :noteId")
suspend fun getById(noteId: Long): NoteEntity? suspend fun getById(noteId: Long): NoteEntity?
@Query("SELECT * FROM notes WHERE pdfHash = :pdfHash ORDER BY pageNumber ASC, createTime ASC") @Query("SELECT * FROM notes WHERE filePath = :filePath ORDER BY pageNumber ASC, createTime ASC")
fun getNotesByPdf(pdfHash: String): Flow<List<NoteEntity>> fun getNotesByPdf(filePath: String): Flow<List<NoteEntity>>
@Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber") @Query("SELECT * FROM notes WHERE filePath = :filePath AND pageNumber = :pageNumber")
suspend fun getNotesByPage(pdfHash: String, pageNumber: Int): List<NoteEntity> suspend fun getNotesByPage(filePath: String, pageNumber: Int): List<NoteEntity>
@Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND noteType = :noteType") @Query("SELECT * FROM notes WHERE filePath = :filePath AND noteType = :noteType")
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> fun getNotesByType(filePath: String, noteType: String): Flow<List<NoteEntity>>
@Query("SELECT COUNT(*) FROM notes WHERE pdfHash = :pdfHash") @Query("SELECT COUNT(*) FROM notes WHERE filePath = :filePath")
suspend fun getNoteCount(pdfHash: String): Int suspend fun getNoteCount(filePath: String): Int
@Query("DELETE FROM notes WHERE pdfHash = :pdfHash") @Query("DELETE FROM notes WHERE filePath = :filePath")
suspend fun deleteAllByPdf(pdfHash: String) suspend fun deleteAllByPdf(filePath: String)
@Query("DELETE FROM notes WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber") @Query("DELETE FROM notes WHERE filePath = :filePath AND pageNumber = :pageNumber")
suspend fun deleteByPage(pdfHash: String, pageNumber: Int) suspend fun deleteByPage(filePath: String, pageNumber: Int)
} }

View File

@ -34,6 +34,6 @@ interface PdfDocumentDao {
@Delete @Delete
suspend fun delete(document: PdfDocumentEntity) suspend fun delete(document: PdfDocumentEntity)
@Query("DELETE FROM pdf_documents WHERE fileHash = :fileHash") @Query("DELETE FROM pdf_documents WHERE filePath = :filePath")
suspend fun deleteByHash(fileHash: String) suspend fun deleteByPath(filePath: String)
} }

View File

@ -11,25 +11,25 @@ interface RecentReadDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(recentRead: RecentReadEntity) suspend fun insertOrUpdate(recentRead: RecentReadEntity)
@Query("SELECT * FROM recently_read WHERE pdfHash = :pdfHash") @Query("SELECT * FROM recently_read WHERE filePath = :filePath")
suspend fun getByPdfHash(pdfHash: String): RecentReadEntity? suspend fun getByPdfHash(filePath: String): RecentReadEntity?
@Query(""" @Query("""
SELECT pdf_documents.* SELECT pdf_documents.*
FROM pdf_documents FROM pdf_documents
INNER JOIN recently_read ON pdf_documents.fileHash = recently_read.pdfHash INNER JOIN recently_read ON pdf_documents.filePath = recently_read.filePath
ORDER BY recently_read.lastOpenedTime DESC ORDER BY recently_read.lastOpenedTime DESC
""") """)
fun getRecentReadDocuments(): Flow<List<PdfDocumentEntity>> fun getRecentReadDocuments(): Flow<List<PdfDocumentEntity>>
@Query("UPDATE recently_read SET lastOpenedTime = :time, openedCount = openedCount + 1 WHERE pdfHash = :pdfHash") @Query("UPDATE recently_read SET lastOpenedTime = :time, openedCount = openedCount + 1 WHERE filePath = :filePath")
suspend fun updateOpenTime(pdfHash: String, time: Long = System.currentTimeMillis()) suspend fun updateOpenTime(filePath: String, time: Long = System.currentTimeMillis())
@Query("UPDATE recently_read SET totalReadTime = totalReadTime + :additionalTime WHERE pdfHash = :pdfHash") @Query("UPDATE recently_read SET totalReadTime = totalReadTime + :additionalTime WHERE filePath = :filePath")
suspend fun addReadTime(pdfHash: String, additionalTime: Long) suspend fun addReadTime(filePath: String, additionalTime: Long)
@Query("DELETE FROM recently_read WHERE pdfHash = :pdfHash") @Query("DELETE FROM recently_read WHERE filePath = :filePath")
suspend fun deleteByPdfHash(pdfHash: String) suspend fun deleteByPdfPath(filePath: String)
@Query("DELETE FROM recently_read WHERE lastOpenedTime < :cutoffTime") @Query("DELETE FROM recently_read WHERE lastOpenedTime < :cutoffTime")
suspend fun deleteOldRecents(cutoffTime: Long) suspend fun deleteOldRecents(cutoffTime: Long)

View File

@ -9,17 +9,17 @@ import androidx.room.PrimaryKey
tableName = "bookmarks", tableName = "bookmarks",
foreignKeys = [ForeignKey( foreignKeys = [ForeignKey(
entity = PdfDocumentEntity::class, entity = PdfDocumentEntity::class,
parentColumns = ["fileHash"], parentColumns = ["filePath"],
childColumns = ["pdfHash"], childColumns = ["filePath"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
)], )],
indices = [Index(value = ["pdfHash"])] indices = [Index(value = ["filePath"])]
) )
data class BookmarkEntity( data class BookmarkEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0,
val pdfHash: String, // 关联PdfDocumentEntity的fileHash val filePath: String, // 关联PdfDocumentEntity的fileHash
val pageNumber: Int, // 页码(从0开始) val pageNumber: Int, // 页码(从0开始)
val label: String, // 书签标签 val label: String, // 书签标签
val positionX: Float = 0f, // 页面内X位置 val positionX: Float = 0f, // 页面内X位置

View File

@ -9,17 +9,17 @@ import androidx.room.PrimaryKey
tableName = "notes", tableName = "notes",
foreignKeys = [ForeignKey( foreignKeys = [ForeignKey(
entity = PdfDocumentEntity::class, entity = PdfDocumentEntity::class,
parentColumns = ["fileHash"], parentColumns = ["filePath"],
childColumns = ["pdfHash"], childColumns = ["filePath"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
)], )],
indices = [Index(value = ["pdfHash"])] indices = [Index(value = ["filePath"])]
) )
data class NoteEntity( data class NoteEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0,
val pdfHash: String, // 关联PdfDocumentEntity的fileHash val filePath: String, // 关联PdfDocumentEntity的fileHash
val pageNumber: Int, // 页码(从0开始) val pageNumber: Int, // 页码(从0开始)
val noteType: String, // 注释类型: HIGHLIGHT, TEXT_NOTE, DRAWING val noteType: String, // 注释类型: HIGHLIGHT, TEXT_NOTE, DRAWING
val content: String, // 注释内容(文本或序列化的绘制数据) val content: String, // 注释内容(文本或序列化的绘制数据)

View File

@ -9,9 +9,9 @@ import kotlinx.parcelize.Parcelize
@Entity(tableName = "pdf_documents") @Entity(tableName = "pdf_documents")
data class PdfDocumentEntity( data class PdfDocumentEntity(
@PrimaryKey @PrimaryKey
val fileHash: String, // 文件内容哈希(MD5/SHA-1)
val filePath: String, // 当前文件路径 val filePath: String, // 当前文件路径
val fileHash: String? = null, // 文件内容哈希(MD5/SHA-1),扫描时不进行获取,需要做判定再单独获取
val fileName: String, // 文件名 val fileName: String, // 文件名
val fileSize: Long, // 文件大小(字节) val fileSize: Long, // 文件大小(字节)
val lastModified: Long, // 文件最后修改时间 val lastModified: Long, // 文件最后修改时间

View File

@ -9,17 +9,17 @@ import androidx.room.PrimaryKey
tableName = "recently_read", tableName = "recently_read",
foreignKeys = [ForeignKey( foreignKeys = [ForeignKey(
entity = PdfDocumentEntity::class, entity = PdfDocumentEntity::class,
parentColumns = ["fileHash"], parentColumns = ["filePath"],
childColumns = ["pdfHash"], childColumns = ["filePath"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
)], )],
indices = [Index(value = ["pdfHash"])] indices = [Index(value = ["filePath"])]
) )
data class RecentReadEntity( data class RecentReadEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0,
val pdfHash: String, // 关联PdfDocumentEntity的fileHash val filePath: String, // 关联PdfDocumentEntity的filePath
val lastOpenedTime: Long, // 最后打开时间 val lastOpenedTime: Long, // 最后打开时间
val openedCount: Int = 1, // 打开次数 val openedCount: Int = 1, // 打开次数
val totalReadTime: Long = 0, // 总阅读时长(毫秒) val totalReadTime: Long = 0, // 总阅读时长(毫秒)

View File

@ -41,16 +41,16 @@ class PdfRepository private constructor(context: Context) {
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> = fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> =
pdfDao.searchDocuments(query) pdfDao.searchDocuments(query)
suspend fun updateFavoriteStatus(fileHash: String, isFavorite: Boolean) { suspend fun updateFavoriteStatus(filePath: String, isFavorite: Boolean) {
val document = pdfDao.getByPath(fileHash)?.copy( val document = pdfDao.getByPath(filePath)?.copy(
isFavorite = isFavorite, isFavorite = isFavorite,
addedToFavoriteTime = if (isFavorite) System.currentTimeMillis() else null addedToFavoriteTime = if (isFavorite) System.currentTimeMillis() else null
) )
document?.let { pdfDao.update(it) } document?.let { pdfDao.update(it) }
} }
suspend fun updateReadingProgress(fileHash: String, page: Int, progress: Float) { suspend fun updateReadingProgress(filePath: String, page: Int, progress: Float) {
val document = pdfDao.getByPath(fileHash)?.copy( val document = pdfDao.getByPath(filePath)?.copy(
lastOpenedTime = System.currentTimeMillis(), lastOpenedTime = System.currentTimeMillis(),
lastReadPage = page, lastReadPage = page,
readingProgress = progress readingProgress = progress
@ -58,29 +58,37 @@ class PdfRepository private constructor(context: Context) {
document?.let { pdfDao.update(it) } document?.let { pdfDao.update(it) }
} }
suspend fun updatePasswordStatus(fileHash: String, isPassword: Boolean) { suspend fun updatePasswordStatus(filePath: String, isPassword: Boolean) {
val document = pdfDao.getByPath(fileHash)?.copy( val document = pdfDao.getByPath(filePath)?.copy(
isPassword = isPassword isPassword = isPassword
) )
document?.let { pdfDao.update(it) } document?.let { pdfDao.update(it) }
} }
suspend fun updatePassword(fileHash: String, password: String?) { suspend fun updatePassword(filePath: String, password: String?) {
val document = pdfDao.getByPath(fileHash)?.copy( val document = pdfDao.getByPath(filePath)?.copy(
password = password password = password
) )
document?.let { pdfDao.update(it) } document?.let { pdfDao.update(it) }
} }
suspend fun updateThumbnailPath(filePath: String, path: String) {
val document = pdfDao.getByPath(filePath)?.copy(
thumbnailPath = path,
)
document?.let { pdfDao.update(it) }
}
// 最近阅读相关操作 // 最近阅读相关操作
suspend fun addToRecent(pdfHash: String, page: Int = 0) { suspend fun addToRecent(filePath: String, page: Int = 0) {
val existing = recentDao.getByPdfHash(pdfHash) val existing = recentDao.getByPdfHash(filePath)
if (existing != null) { if (existing != null) {
recentDao.updateOpenTime(pdfHash) recentDao.updateOpenTime(filePath)
} else { } else {
recentDao.insertOrUpdate( recentDao.insertOrUpdate(
RecentReadEntity( RecentReadEntity(
pdfHash = pdfHash, filePath = filePath,
lastOpenedTime = System.currentTimeMillis() lastOpenedTime = System.currentTimeMillis()
) )
) )
@ -113,7 +121,7 @@ class PdfRepository private constructor(context: Context) {
// 组合查询 // 组合查询
suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> { suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> {
return combine( return combine(
pdfDao.getByPath(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) } pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) }
?: kotlinx.coroutines.flow.flowOf(null), ?: kotlinx.coroutines.flow.flowOf(null),
bookmarkDao.getBookmarksByPdf(pdfHash), bookmarkDao.getBookmarksByPdf(pdfHash),
noteDao.getNotesByPdf(pdfHash) noteDao.getNotesByPdf(pdfHash)
@ -144,11 +152,11 @@ class PdfRepository private constructor(context: Context) {
} }
// 数据清理 // 数据清理
suspend fun deleteDocument(fileHash: String) { suspend fun deleteDocument(filePath: String) {
pdfDao.deleteByHash(fileHash) pdfDao.deleteByPath(filePath)
recentDao.deleteByPdfHash(fileHash) recentDao.deleteByPdfPath(filePath)
bookmarkDao.deleteAllByPdf(fileHash) bookmarkDao.deleteAllByPdf(filePath)
noteDao.deleteAllByPdf(fileHash) noteDao.deleteAllByPdf(filePath)
} }
companion object { companion object {

View File

@ -10,16 +10,12 @@ import com.all.pdfreader.pro.app.PDFReaderApplication
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.SortDialogFragment
import com.all.pdfreader.pro.app.ui.fragment.FavoriteFrag import com.all.pdfreader.pro.app.ui.fragment.FavoriteFrag
import com.all.pdfreader.pro.app.ui.fragment.HomeFrag import com.all.pdfreader.pro.app.ui.fragment.HomeFrag
import com.all.pdfreader.pro.app.ui.fragment.RecentlyFrag import com.all.pdfreader.pro.app.ui.fragment.RecentlyFrag
import com.all.pdfreader.pro.app.ui.fragment.ToolsFrag import com.all.pdfreader.pro.app.ui.fragment.ToolsFrag
import com.all.pdfreader.pro.app.model.SortConfig
import com.all.pdfreader.pro.app.model.SortField
import com.all.pdfreader.pro.app.model.SortDirection
import com.all.pdfreader.pro.app.ui.dialog.SortDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation
import com.all.pdfreader.pro.app.util.FileChangeObserver
import com.all.pdfreader.pro.app.util.PdfScanner import com.all.pdfreader.pro.app.util.PdfScanner
import com.all.pdfreader.pro.app.util.StoragePermissionHelper import com.all.pdfreader.pro.app.util.StoragePermissionHelper
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar

View File

@ -123,7 +123,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
private fun saveReadingProgress() { private fun saveReadingProgress() {
lifecycleScope.launch { lifecycleScope.launch {
repository.updateReadingProgress( repository.updateReadingProgress(
pdfDocument.fileHash, pdfDocument.lastReadPage, pdfDocument.readingProgress pdfDocument.filePath, pdfDocument.lastReadPage, pdfDocument.readingProgress
) )
} }
} }

View File

@ -0,0 +1,128 @@
package com.all.pdfreader.pro.app.ui.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogListMoreBinding
import com.all.pdfreader.pro.app.databinding.DialogPermissionBinding
import com.all.pdfreader.pro.app.databinding.DialogSortBinding
import com.all.pdfreader.pro.app.model.SortConfig
import com.all.pdfreader.pro.app.model.SortDirection
import com.all.pdfreader.pro.app.model.SortField
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.sp.AppStore
import com.all.pdfreader.pro.app.ui.act.MainActivity.SortableFragment
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation
import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize
import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.launch
class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment() {
private lateinit var binding: DialogListMoreBinding
private val pdfRepository = PdfRepository.getInstance()
private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] }
private lateinit var pdfDocument: PdfDocumentEntity
private var isFavorite: Boolean = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
binding = DialogListMoreBinding.inflate(layoutInflater)
return binding.root
}
override fun onStart() {
super.onStart()
dialog?.window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
?.setBackgroundResource(R.drawable.dr_rounded_corner_12_bg_white)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.pdfDocument.observe(this) { document ->
document?.let {
pdfDocument = it
isFavorite = pdfDocument.isFavorite
initUi()
setupOnClick()
} ?: run {
showToast(getString(R.string.file_not))
dismiss()
}
}
viewModel.getPDFDocument(filePath)
}
private fun initUi() {
binding.tvFileName.text = pdfDocument.fileName
binding.tvFileSize.text = pdfDocument.fileSize.toFormatFileSize()
binding.tvFileDate.text = pdfDocument.lastModified.toSlashDate()
if (pdfDocument.isPassword) {
binding.lockLayout.visibility = View.VISIBLE
binding.tvFileImg.visibility = View.GONE
} else {
binding.lockLayout.visibility = View.GONE
binding.tvFileImg.visibility = View.VISIBLE
Glide.with(binding.root).load(pdfDocument.thumbnailPath)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(binding.root.context)))
.into(binding.tvFileImg)
}
updateCollectUi(isFavorite)
}
private fun setupOnClick() {
binding.collectBtn.setClickWithAnimation(duration = 250) {
isFavorite = !isFavorite
updateCollectUi(isFavorite)
saveCollectState(isFavorite)
}
binding.renameFileBtn.setOnClickListener {
RenameDialogFragment(pdfDocument.filePath, onOkClick = {
}, onCancelClick = {
}).show(parentFragmentManager, "ListMoreDialogFragment")
dismiss()
}
}
private fun saveCollectState(b: Boolean) {
pdfDocument = pdfDocument.copy(
isFavorite = b
)
lifecycleScope.launch {
pdfRepository.updateFavoriteStatus(pdfDocument.filePath, pdfDocument.isFavorite)
}
if (b) {
showToast(getString(R.string.added_to_favorites))
} else {
showToast(getString(R.string.removed_from_favorites))
}
}
private fun updateCollectUi(b: Boolean) {
if (b) {
binding.collectIv.setImageResource(R.drawable.collected)
} else {
binding.collectIv.setImageResource(R.drawable.collect)
}
}
private fun showToast(message: String) {
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
}
}

View File

@ -2,7 +2,6 @@ package com.all.pdfreader.pro.app.ui.dialog
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -21,7 +20,6 @@ class PdfPasswordProtectionDialogFragment(
) : DialogFragment() { ) : DialogFragment() {
private lateinit var binding: DialogPdfPasswordProtectionBinding private lateinit var binding: DialogPdfPasswordProtectionBinding
var isPasswordVisible = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@ -67,23 +65,5 @@ class PdfPasswordProtectionDialogFragment(
binding.tilPassword.error = getString(R.string.incorrect_password) binding.tilPassword.error = getString(R.string.incorrect_password)
} }
} }
binding.showPasswordBtn.setOnClickListener {
isPasswordVisible = !isPasswordVisible
showOrHidePassword(isPasswordVisible)
}
}
private fun showOrHidePassword(b: Boolean) {
if (b) {
binding.etPassword.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
binding.showPasswordIv.setImageResource(R.drawable.show_password)
} else {
binding.etPassword.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
binding.showPasswordIv.setImageResource(R.drawable.hide_password)
}
// 保持光标在末尾
binding.etPassword.setSelection(binding.etPassword.text?.length ?: 0)
} }
} }

View File

@ -3,18 +3,15 @@ package com.all.pdfreader.pro.app.ui.dialog
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.InputType
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogPdfSetPasswordBinding import com.all.pdfreader.pro.app.databinding.DialogPdfSetPasswordBinding
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.google.android.material.textfield.TextInputEditText
class PdfSetPasswordDialog( class PdfSetPasswordDialog(
private val onCancelled: () -> Unit, private val onPasswordSet: (String) -> Unit private val onCancelled: () -> Unit, private val onPasswordSet: (String) -> Unit
@ -22,8 +19,6 @@ class PdfSetPasswordDialog(
) { ) {
private lateinit var binding: DialogPdfSetPasswordBinding private lateinit var binding: DialogPdfSetPasswordBinding
private var isEnterPasswordVisible = false
private var isConfirmPasswordVisible = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@ -66,23 +61,6 @@ class PdfSetPasswordDialog(
dismiss() dismiss()
} }
} }
binding.enterShowPasswordBtn.setOnClickListener {
isEnterPasswordVisible = !isEnterPasswordVisible
showOrHidePassword(
binding.etPassword,
binding.enterShowPasswordIv,
isEnterPasswordVisible
)
}
binding.confirmShowPasswordBtn.setOnClickListener {
isConfirmPasswordVisible = !isConfirmPasswordVisible
showOrHidePassword(
binding.etConfirmPassword,
binding.confirmShowPasswordIv,
isConfirmPasswordVisible
)
}
} }
private fun setupTextWatchers() { private fun setupTextWatchers() {
@ -140,17 +118,4 @@ class PdfSetPasswordDialog(
return true return true
} }
private fun showOrHidePassword(inputView: TextInputEditText, imageView: ImageView, b: Boolean) {
if (b) {
inputView.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
imageView.setImageResource(R.drawable.show_password)
} else {
inputView.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
imageView.setImageResource(R.drawable.hide_password)
}
// 保持光标在末尾
inputView.setSelection(inputView.text?.length ?: 0)
}
} }

View File

@ -0,0 +1,135 @@
package com.all.pdfreader.pro.app.ui.dialog
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogPdfPasswordProtectionBinding
import com.all.pdfreader.pro.app.databinding.DialogRenameFileBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.all.pdfreader.pro.app.util.FileUtils.isPdfPasswordCorrect
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import java.io.File
class RenameDialogFragment(
private val filePath: String,
private val onOkClick: () -> Unit,
private val onCancelClick: () -> Unit
) : DialogFragment() {
private lateinit var binding: DialogRenameFileBinding
private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] }
private lateinit var pdfDocument: PdfDocumentEntity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
binding = DialogRenameFileBinding.inflate(layoutInflater)
return binding.root
}
override fun onStart() {
super.onStart()
dialog?.window?.apply {
// 去掉系统默认的背景 padding
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
// 设置宽度为全屏减去 16dp
val margin = resources.getDimensionPixelSize(R.dimen.dialog_margin) // 16dp
val width = resources.displayMetrics.widthPixels - margin * 2
setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.pdfDocument.observe(this) { document ->
document?.let {
pdfDocument = it
initView()
setupOnClick()
} ?: run {
showToast(getString(R.string.file_not))
dismiss()
}
}
viewModel.getPDFDocument(filePath)
}
private fun initView() {
binding.etName.showKeyboard()
binding.etName.setText(pdfDocument.fileName)
// 保持光标在末尾
binding.etName.setSelection(binding.etName.text?.length ?: 0)
}
private fun setupOnClick() {
binding.tvCancel.setOnClickListener {
onCancelClick()
dismiss()
}
binding.tvConfirm.setOnClickListener {
val text = binding.etName.text.toString()
if (validateEnter(text)) {
}
}
}
private fun validateEnter(name: String): Boolean {
// 不允许为空
if (name.isBlank()) {
binding.tilName.error = getString(R.string.name_not_empty)
return false
}
// 名字未做修改
if (name == pdfDocument.fileName) {
binding.tilName.error = getString(R.string.name_not_changed)
return false
}
// 含有非法字符
val invalidChars = "[/\\\\:*?\"<>|]".toRegex()
if (invalidChars.containsMatchIn(name)) {
binding.tilName.error = getString(R.string.name_invalid_chars)
return false
}
// 长度过长
if (name.length > 255) {
binding.tilName.error = getString(R.string.name_too_long)
return false
}
// 与现有文件重名
val parentDir = File(pdfDocument.filePath).parentFile
if (parentDir != null && File(parentDir, name).exists()) {
binding.tilName.error = getString(R.string.name_already_exists)
return false
}
// 禁止开头/结尾空格
if (name != name.trim()) {
binding.tilName.error = getString(R.string.name_start_end_space)
return false
}
return true
}
private fun showToast(message: String) {
Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
}
}

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogPermissionBinding import com.all.pdfreader.pro.app.databinding.DialogPermissionBinding
import com.all.pdfreader.pro.app.databinding.DialogSortBinding import com.all.pdfreader.pro.app.databinding.DialogSortBinding
import com.all.pdfreader.pro.app.model.SortConfig import com.all.pdfreader.pro.app.model.SortConfig
@ -30,6 +31,12 @@ class SortDialogFragment(
return binding.root return binding.root
} }
override fun onStart() {
super.onStart()
dialog?.window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
?.setBackgroundResource(R.drawable.dr_rounded_corner_12_bg_white)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val appStore = AppStore(requireActivity()) val appStore = AppStore(requireActivity())

View File

@ -15,6 +15,7 @@ import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.ui.act.MainActivity import com.all.pdfreader.pro.app.ui.act.MainActivity
import com.all.pdfreader.pro.app.ui.act.PdfViewActivity import com.all.pdfreader.pro.app.ui.act.PdfViewActivity
import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.ListMoreDialogFragment
import com.all.pdfreader.pro.app.util.PdfScanner import com.all.pdfreader.pro.app.util.PdfScanner
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,7 +41,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath) val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent) startActivity(intent)
}, onMoreClick = { pdf -> }, onMoreClick = { pdf ->
ListMoreDialogFragment(pdf.filePath).show(parentFragmentManager, TAG)
}) })
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())

View File

@ -14,7 +14,11 @@ object AppUtils {
* *
* @param onClick 点击后立即执行的逻辑 * @param onClick 点击后立即执行的逻辑
*/ */
fun View.setClickWithAnimation(onClick: () -> Unit) { fun View.setClickWithAnimation(
scaleFactor: Float = 1.2f,
duration: Long = 150,
onClick: () -> Unit
) {
this.setOnClickListener { this.setOnClickListener {
// 禁用点击,防止动画未完成重复点击 // 禁用点击,防止动画未完成重复点击
this.isEnabled = false this.isEnabled = false
@ -24,14 +28,14 @@ object AppUtils {
// 播放动画 // 播放动画
this.animate() this.animate()
.scaleX(1.2f) .scaleX(scaleFactor)
.scaleY(1.2f) .scaleY(scaleFactor)
.setDuration(150) .setDuration(duration)
.withEndAction { .withEndAction {
this.animate() this.animate()
.scaleX(1f) .scaleX(1f)
.scaleY(1f) .scaleY(1f)
.setDuration(150) .setDuration(duration)
.withEndAction { .withEndAction {
// 动画结束恢复点击 // 动画结束恢复点击
this.isEnabled = true this.isEnabled = true

View File

@ -237,6 +237,9 @@ object FileUtils {
} }
} }
/**
* 计算文件哈希
*/
fun calculateFileHash(filePath: String): String? { fun calculateFileHash(filePath: String): String? {
return try { return try {
val file = File(filePath) val file = File(filePath)

View File

@ -2,9 +2,7 @@ package com.all.pdfreader.pro.app.util
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@ -12,7 +10,10 @@ import com.all.pdfreader.pro.app.PDFReaderApplication
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
import com.shockwave.pdfium.PdfDocument
import com.shockwave.pdfium.PdfiumCore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -29,7 +30,9 @@ class PdfScanner(
suspend fun scanAndLoadPdfFiles(isNeedFullScan: Boolean, callback: (Boolean) -> Unit = {}) { suspend fun scanAndLoadPdfFiles(isNeedFullScan: Boolean, callback: (Boolean) -> Unit = {}) {
if (!StoragePermissionHelper.hasBasicStoragePermission(context)) { if (!StoragePermissionHelper.hasBasicStoragePermission(context)) {
LogUtil.logDebug(TAG, "权限不足") LogUtil.logDebug(TAG, "权限不足")
withContext(Dispatchers.Main) {
callback.invoke(false) callback.invoke(false)
}
return return
} }
scanMutex.withLock {// 保证同一时间只有一次扫描 scanMutex.withLock {// 保证同一时间只有一次扫描
@ -64,23 +67,30 @@ class PdfScanner(
LogUtil.logDebug(TAG, "密码状态变化 -> ${doc.fileName}") LogUtil.logDebug(TAG, "密码状态变化 -> ${doc.fileName}")
updatedDoc = updatedDoc.copy(isPassword = currentIsPassword) updatedDoc = updatedDoc.copy(isPassword = currentIsPassword)
} }
// 缩略图(仅非加密文件)
if (!currentIsPassword) {
val newThumbnail = generateThumbnail(context, file)
if (doc.thumbnailPath != newThumbnail) {
updatedDoc =
updatedDoc.copy(thumbnailPath = newThumbnail)
}
} else if (doc.thumbnailPath != null) {
updatedDoc = updatedDoc.copy(thumbnailPath = null)
}
pdfRepository.insertOrUpdateDocument(updatedDoc) pdfRepository.insertOrUpdateDocument(updatedDoc)
LogUtil.logDebug(TAG, "✅数据库已更新: ${doc.fileName}") LogUtil.logDebug(TAG, "✅数据库已更新: ${doc.fileName}")
if (!currentIsPassword) {
// 异步生成缩略图,但要避免阻塞
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && doc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(doc.filePath, newThumbnail)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
}
} else if (doc.thumbnailPath != null) {
val updatedDocWithoutThumb =
updatedDoc.copy(thumbnailPath = null)
pdfRepository.insertOrUpdateDocument(updatedDocWithoutThumb)
LogUtil.logDebug(TAG, "✅图片为Null: ${doc.fileName}")
}
} }
} else { } else {
// 文件不存在 → 删除数据库记录,并触发全盘扫描 // 文件不存在 → 删除数据库记录,并触发全盘扫描
LogUtil.logDebug(TAG, "文件不存在 -> ${doc.fileName}, 删除记录") LogUtil.logDebug(TAG, "文件不存在 -> ${doc.fileName}, 删除记录")
pdfRepository.deleteDocument(doc.fileHash) pdfRepository.deleteDocument(doc.filePath)
needFullScan = true needFullScan = true
} }
} }
@ -103,12 +113,7 @@ class PdfScanner(
TAG, TAG,
"🔄处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}" "🔄处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}"
) )
if (FileUtils.isPdfFile(file)) { if (FileUtils.isPdfFile(file)) {
val fileHash = FileUtils.calculateFileHash(file.absolutePath)
LogUtil.logDebug(TAG, "🔑文件哈希: $fileHash")
if (fileHash != null) {
val existingDoc = val existingDoc =
pdfRepository.getDocumentByPath(file.absolutePath) pdfRepository.getDocumentByPath(file.absolutePath)
@ -116,23 +121,18 @@ class PdfScanner(
LogUtil.logDebug( LogUtil.logDebug(
TAG, "🆕发现新PDF文件: ${file.name}" TAG, "🆕发现新PDF文件: ${file.name}"
) )
var thumbnailPath: String? = null
val isPassword = isPdfEncrypted(file) val isPassword = isPdfEncrypted(file)
LogUtil.logDebug(TAG, "isPassword->${isPassword}") LogUtil.logDebug(TAG, "isPassword->${isPassword}")
if (!isPassword) {//没有密码的情况下才去获取缩略图
thumbnailPath = generateThumbnail(context, file) ?: ""
}
LogUtil.logDebug(TAG, "thumbnailPath->${thumbnailPath}")
val metadata = val metadata =
PdfMetadataExtractor.extractMetadata(file.absolutePath) PdfMetadataExtractor.extractMetadata(file.absolutePath)
val document = PdfDocumentEntity( val document = PdfDocumentEntity(
fileHash = fileHash,
filePath = file.absolutePath, filePath = file.absolutePath,
fileName = file.name, fileName = file.name,
fileSize = file.length(), fileSize = file.length(),
lastModified = file.lastModified(), lastModified = file.lastModified(),
pageCount = metadata?.pageCount ?: 0, pageCount = metadata?.pageCount ?: 0,
thumbnailPath = thumbnailPath, thumbnailPath = null,
metadataTitle = metadata?.title, metadataTitle = metadata?.title,
metadataAuthor = metadata?.author, metadataAuthor = metadata?.author,
metadataSubject = metadata?.subject, metadataSubject = metadata?.subject,
@ -142,9 +142,17 @@ class PdfScanner(
isPassword = isPassword isPassword = isPassword
) )
pdfRepository.insertOrUpdateDocument(document) pdfRepository.insertOrUpdateDocument(document)
LogUtil.logDebug( LogUtil.logDebug(TAG, " ✅ 已保存到数据库: ${file.name}")
TAG, " ✅ 已保存到数据库: ${file.name}"
) if (!isPassword) {//没有密码的情况下才去获取缩略图
launch(Dispatchers.IO){
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && document.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(document.filePath, newThumbnail)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
}
}
} else { } else {
LogUtil.logDebug(TAG, " 📋 文件已存在: ${file.name}") LogUtil.logDebug(TAG, " 📋 文件已存在: ${file.name}")
// 🔹 文件已存在,检查是否需要更新 // 🔹 文件已存在,检查是否需要更新
@ -170,31 +178,27 @@ class PdfScanner(
needUpdate = true needUpdate = true
} }
if (!currentIsPassword) {
// 如果不是加密 PDF再生成缩略图
val newThumbnail = generateThumbnail(context, file)
if (existingDoc.thumbnailPath != newThumbnail) {
LogUtil.logDebug(TAG, "✅ 缩略图需要更新")
updatedDoc =
updatedDoc.copy(thumbnailPath = newThumbnail)
needUpdate = true
}
} else {
updatedDoc = updatedDoc.copy(thumbnailPath = null)
needUpdate = true
}
// 执行更新 // 执行更新
if (needUpdate) { if (needUpdate) {
pdfRepository.insertOrUpdateDocument(updatedDoc) pdfRepository.insertOrUpdateDocument(updatedDoc)
LogUtil.logDebug( LogUtil.logDebug(TAG, "✅ 数据库已更新: ${file.name}")
TAG, "✅ 数据库已更新: ${file.name}"
)
} else { } else {
LogUtil.logDebug( LogUtil.logDebug(TAG, "⏩ 无需更新: ${file.name}")
TAG, "⏩ 无需更新: ${file.name}"
)
} }
// 处理缩略图
if (!currentIsPassword) {
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && existingDoc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(existingDoc.filePath, newThumbnail)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
}
} else {
val noThumbDoc = updatedDoc.copy(thumbnailPath = null)
pdfRepository.insertOrUpdateDocument(noThumbDoc)
} }
} }
} }
@ -202,7 +206,8 @@ class PdfScanner(
// 打印数据库中的总记录数 // 打印数据库中的总记录数
pdfRepository.getAllDocumentsOnce().forEach { doc -> pdfRepository.getAllDocumentsOnce().forEach { doc ->
LogUtil.logDebug( LogUtil.logDebug(
TAG, " 📖 ${doc.fileName} - ${doc.pageCount}页 - ${ TAG,
" 📖 ${doc.fileName} - ${doc.filePath} - ${doc.pageCount}页 - ${
FileUtils.formatFileSize( FileUtils.formatFileSize(
doc.fileSize doc.fileSize
) )
@ -229,14 +234,18 @@ class PdfScanner(
TAG, "$string 本次扫描耗时: $scannerTime ms (${scannerTime / 1000.0} 秒)" TAG, "$string 本次扫描耗时: $scannerTime ms (${scannerTime / 1000.0} 秒)"
) )
PDFReaderApplication.isNeedFullScan = false PDFReaderApplication.isNeedFullScan = false
withContext(Dispatchers.Main) {
callback.invoke(true) callback.invoke(true)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ 扫描出错: ${e.message}", e) Log.e(TAG, "❌ 扫描出错: ${e.message}", e)
withContext(Dispatchers.Main) {
callback.invoke(false) callback.invoke(false)
} }
} }
} }
} }
}
fun shouldScan(): Boolean { fun shouldScan(): Boolean {
return ScanManager.shouldScan(context) return ScanManager.shouldScan(context)
@ -252,40 +261,87 @@ class PdfScanner(
} }
private fun generateThumbnail(context: Context, pdfFile: File): String? { // private fun generateFastThumbnail(context: Context, pdfFile: File): String? {
// return try {
// val fileDescriptor =
// ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
// val pdfRenderer = PdfRenderer(fileDescriptor)
//
// if (pdfRenderer.pageCount > 0) {
// val page = pdfRenderer.openPage(0)
// // 创建 Bitmap
// val bitmap = createBitmap(page.width, page.height)
// val canvas = Canvas(bitmap)
// canvas.drawColor(Color.WHITE) // 填充白色背景
//
// page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
// page.close()
//
// // 保存到缓存目录
// val cacheDir = File(context.cacheDir, "thumbnails")
// if (!cacheDir.exists()) cacheDir.mkdirs()
// val thumbFile = File(cacheDir, pdfFile.nameWithoutExtension + ".jpg")
//
// FileOutputStream(thumbFile).use { out ->
// bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
// }
//
// pdfRenderer.close()
// fileDescriptor.close()
//
// thumbFile.absolutePath
// } else {
// pdfRenderer.close()
// fileDescriptor.close()
// null
// }
// } catch (e: Exception) {
// e.printStackTrace()
// null
// }
// }
private fun generateFastThumbnail(
context: Context,
pdfFile: File,
maxWidth: Int = 200,
maxHeight: Int = 300
): String? {
return try { return try {
val fileDescriptor = val pdfiumCore = PdfiumCore(context)
ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY) val fd = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
val pdfRenderer = PdfRenderer(fileDescriptor) val pdfDocument: PdfDocument = pdfiumCore.newDocument(fd)
if (pdfRenderer.pageCount > 0) { // 打开第一页
val page = pdfRenderer.openPage(0) pdfiumCore.openPage(pdfDocument, 0)
// 创建 Bitmap
val bitmap = createBitmap(page.width, page.height)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE) // 填充白色背景
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) // 获取原始页宽高
page.close() val width = pdfiumCore.getPageWidthPoint(pdfDocument, 0)
val height = pdfiumCore.getPageHeightPoint(pdfDocument, 0)
// 保存到缓存目录 // 计算缩略图尺寸
val scale = minOf(maxWidth.toFloat() / width, maxHeight.toFloat() / height)
val thumbWidth = (width * scale).toInt()
val thumbHeight = (height * scale).toInt()
val bitmap = createBitmap(thumbWidth, thumbHeight)
bitmap.eraseColor(Color.WHITE) // 白底
// 渲染第一页到 Bitmap
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, 0, 0, 0, thumbWidth, thumbHeight)
// 保存到缓存
val cacheDir = File(context.cacheDir, "thumbnails") val cacheDir = File(context.cacheDir, "thumbnails")
if (!cacheDir.exists()) cacheDir.mkdirs() if (!cacheDir.exists()) cacheDir.mkdirs()
val thumbFile = File(cacheDir, pdfFile.nameWithoutExtension + ".jpg") val thumbFile = File(cacheDir, pdfFile.nameWithoutExtension + ".jpg")
FileOutputStream(thumbFile).use { out -> FileOutputStream(thumbFile).use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out) bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
} }
pdfRenderer.close() pdfiumCore.closeDocument(pdfDocument)
fileDescriptor.close() fd.close()
thumbFile.absolutePath thumbFile.absolutePath
} else {
pdfRenderer.close()
fileDescriptor.close()
null
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M859.7,253.9c-44.8,-44.8 -102.4,-70.4 -166.4,-70.4 -61.9,0 -121.6,25.6 -166.4,70.4l-14.9,17.1 -17.1,-17.1c-44.8,-44.8 -102.4,-70.4 -166.4,-70.4 -61.9,0 -121.6,25.6 -166.4,70.4 -91.7,91.7 -91.7,243.2 0,337.1l324.3,330.7c6.4,6.4 14.9,8.5 23.5,8.5s17.1,-4.3 23.5,-8.5l324.3,-330.7c44.8,-44.8 68.3,-104.5 68.3,-168.5s-21.3,-123.7 -66.1,-168.5zM814.9,544L512,853.3 209.1,544c-66.1,-68.3 -66.1,-179.2 0,-247.5 32,-32 74.7,-51.2 119.5,-51.2 44.8,0 87.5,17.1 119.5,51.2l38.4,40.5c12.8,12.8 34.1,12.8 44.8,0l38.4,-40.5c32,-32 74.7,-51.2 119.5,-51.2 44.8,0 87.5,17.1 119.5,51.2 32,32 49.1,76.8 49.1,123.7s-12.8,91.7 -42.7,123.7z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M669.8,130.8c71.6,-11.1 138.9,11.5 193.3,64.5 55.3,53.9 81.8,125 74.3,199.5 -7.5,73.6 -46.5,146.4 -112.3,210.5 -18.3,17.9 -67.7,66.2 -138.5,135.6 -31.8,31.2 -65.7,64.4 -99.8,98L553.6,871.5l-13.2,12.9a40.6,40.6 0,0 1,-56.8 0l-114.6,-112.6 -24.2,-23.7a677626.4,677626.4 0,0 0,-145.9 -142.8C133.1,541.2 94.1,468.5 86.6,394.8c-7.6,-74.5 18.9,-145.6 74.3,-199.5 54.4,-53.1 121.7,-75.6 193.3,-64.5 53.2,8.2 107.1,34.7 157.8,76.9 50.7,-42.2 104.6,-68.7 157.8,-76.9z"
android:fillColor="#F1494F"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M874.7,202.7L360.5,202.7c-21.3,0 -40.5,8.5 -55.5,23.5l-217.6,234.7c-25.6,27.7 -25.6,72.5 0,100.3l217.6,234.7c14.9,14.9 34.1,23.5 55.5,23.5L874.7,819.2c40.5,0 74.7,-34.1 74.7,-74.7L949.3,277.3c0,-40.5 -34.1,-74.7 -74.7,-74.7zM885.3,746.7c0,6.4 -4.3,10.7 -10.7,10.7L360.5,757.3c-2.1,0 -6.4,-2.1 -8.5,-4.3l-217.6,-234.7c-4.3,-4.3 -4.3,-10.7 0,-14.9l217.6,-234.7c2.1,-2.1 4.3,-4.3 8.5,-4.3L874.7,264.5c6.4,0 10.7,4.3 10.7,10.7L885.3,746.7z"
android:fillColor="#666666"/>
<path
android:pathData="M684.8,403.2c-12.8,-12.8 -32,-12.8 -44.8,0l-64,64 -61.9,-61.9c-12.8,-12.8 -32,-12.8 -44.8,0 -12.8,12.8 -12.8,32 0,44.8l61.9,61.9 -61.9,61.9c-12.8,12.8 -12.8,32 0,44.8 6.4,6.4 14.9,8.5 23.5,8.5s17.1,-2.1 23.5,-8.5l61.9,-61.9L640,618.7c6.4,6.4 14.9,8.5 23.5,8.5s17.1,-2.1 23.5,-8.5c12.8,-12.8 12.8,-32 0,-44.8L620.8,512l61.9,-61.9c12.8,-12.8 12.8,-34.1 2.1,-46.9z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<size
android:width="24dp"
android:height="4dp" />
<solid android:color="@color/line_color" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/show_password" android:state_checked="true"/>
<item android:drawable="@drawable/hide_password"/>
</selector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M853.3,224h-53.3L800,170.7c0,-40.5 -34.1,-74.7 -74.7,-74.7L170.7,96C130.1,96 96,130.1 96,170.7v554.7c0,40.5 34.1,74.7 74.7,74.7h53.3L224,853.3c0,40.5 34.1,74.7 74.7,74.7h554.7c40.5,0 74.7,-34.1 74.7,-74.7L928,298.7c0,-40.5 -34.1,-74.7 -74.7,-74.7zM160,725.3L160,170.7c0,-6.4 4.3,-10.7 10.7,-10.7h554.7c6.4,0 10.7,4.3 10.7,10.7v554.7c0,6.4 -4.3,10.7 -10.7,10.7L170.7,736c-6.4,0 -10.7,-4.3 -10.7,-10.7zM864,853.3c0,6.4 -4.3,10.7 -10.7,10.7L298.7,864c-6.4,0 -10.7,-4.3 -10.7,-10.7v-53.3L725.3,800c40.5,0 74.7,-34.1 74.7,-74.7L800,288L853.3,288c6.4,0 10.7,4.3 10.7,10.7v554.7z"
android:fillColor="#666666"/>
<path
android:pathData="M576,416h-96V320c0,-17.1 -14.9,-32 -32,-32s-32,14.9 -32,32v96H320c-17.1,0 -32,14.9 -32,32s14.9,32 32,32h96V576c0,17.1 14.9,32 32,32s32,-14.9 32,-32v-96H576c17.1,0 32,-14.9 32,-32s-14.9,-32 -32,-32z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M842.7,285.9l-187.7,-187.7c-14.9,-14.9 -32,-21.3 -53.3,-21.3L234.7,76.8C194.1,74.7 160,108.8 160,149.3v725.3c0,40.5 34.1,74.7 74.7,74.7h554.7c40.5,0 74.7,-34.1 74.7,-74.7L864,337.1c0,-19.2 -8.5,-38.4 -21.3,-51.2zM797.9,330.7c-2.1,2.1 -4.3,0 -8.5,0h-170.7c-6.4,0 -10.7,-4.3 -10.7,-10.7L608,149.3c0,-2.1 0,-6.4 -2.1,-8.5 0,0 2.1,0 2.1,2.1l189.9,187.7zM789.3,885.3L234.7,885.3c-6.4,0 -10.7,-4.3 -10.7,-10.7L224,149.3c0,-6.4 4.3,-10.7 10.7,-10.7h311.5c-2.1,4.3 -2.1,6.4 -2.1,10.7v170.7c0,40.5 34.1,74.7 74.7,74.7h170.7c4.3,0 6.4,0 10.7,-2.1L800,874.7c0,6.4 -4.3,10.7 -10.7,10.7z"
android:fillColor="#666666"/>
<path
android:pathData="M640,586.7H384c-17.1,0 -32,14.9 -32,32s14.9,32 32,32h256c17.1,0 32,-14.9 32,-32s-14.9,-32 -32,-32z"
android:fillColor="#666666"/>
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="366.8dp" android:width="24dp"
android:height="256dp" android:height="17dp"
android:viewportWidth="1467" android:viewportWidth="1467"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M299.1,18.5l917.2,917.2a34.1,34.1 0,0 1,-48.3 48.3L250.9,66.7A34.1,34.1 0,1 1,299.1 18.5zM324.6,188.7l50.6,50.6a918.1,918.1 0,0 0,-113.9 67.5,829.3 829.3,0 0,0 -93.1,75.2l-14.1,13.6c-4.5,4.5 -8.9,8.9 -13.1,13.3l-12,12.9A438.3,438.3 0,0 0,99.7 457.4l-7.3,10.6c-10,15.2 -15.5,27.6 -15.9,35.9 -0.6,15.4 23.2,51.4 64.1,93.3l12.7,12.8c28.7,27.9 64.2,57.8 104.4,85.5 140.2,96.5 308.7,155.6 475.9,155.6 75.1,0 150,-11 222.1,-31.2l54.7,54.7c-85.3,27.7 -178.8,44.7 -276.8,44.7 -256.6,0 -483.8,-126.3 -612.9,-246l-13.2,-12.6c-12.9,-12.6 -24.6,-25 -35.2,-37.1l-10.1,-12a468.4,468.4 0,0 1,-4.8 -5.9l-8.9,-11.7C21.6,557.3 7.3,524.7 8.3,501.2c0.9,-21.5 13.8,-50.7 37,-83.5l8.6,-11.7 9.5,-12 5.1,-6.1 10.7,-12.4 5.6,-6.2 11.9,-12.6 12.6,-12.8c4.4,-4.3 8.8,-8.6 13.4,-12.9l14.1,-12.9c12.1,-10.8 24.9,-21.6 38.4,-32.3l16.6,-12.8a976.1,976.1 0,0 1,132.7 -84.2zM733.6,83c400.6,0 725.3,284.8 725.3,418.1 0,42.4 -32.9,100.2 -90.6,159.2l-12.8,12.7 -6.7,6.3 -13.9,12.7c-4.7,4.2 -9.6,8.4 -14.6,12.6l-15.3,12.6c-5.2,4.2 -10.5,8.3 -16,12.5l-16.6,12.4a929.4,929.4 0,0 1,-125.5 76.1l-51,-50.9a880.5,880.5 0,0 0,112.4 -63.8c58.2,-39 106.9,-82.3 140.3,-123.6 28.1,-34.7 41.9,-64.1 41.9,-78.7 0,-14.6 -13.8,-44 -41.8,-78.7 -33.4,-41.3 -82.1,-84.7 -140.3,-123.6 -136.9,-91.6 -305.5,-147.6 -474.9,-147.6 -74.3,0 -148.5,11.7 -220.2,33.2l-54.1,-54.2c84.4,-29.1 177,-47.2 274.4,-47.2zM536.7,400.8l52,52a152.7,152.7 0,0 0,193.2 193.2l52,52.1a220.9,220.9 0,0 1,-297.2 -297.3zM733.6,280.2a220.9,220.9 0,0 1,196.9 321.3L878.5,549.5a152.7,152.7 0,0 0,-193.2 -193.2L633.2,304.3a220,220 0,0 1,100.4 -24.1z" android:pathData="M299.1,18.5l917.2,917.2a34.1,34.1 0,0 1,-48.3 48.3L250.9,66.7A34.1,34.1 0,1 1,299.1 18.5zM324.6,188.7l50.6,50.6a918.1,918.1 0,0 0,-113.9 67.5,829.3 829.3,0 0,0 -93.1,75.2l-14.1,13.6c-4.5,4.5 -8.9,8.9 -13.1,13.3l-12,12.9A438.3,438.3 0,0 0,99.7 457.4l-7.3,10.6c-10,15.2 -15.5,27.6 -15.9,35.9 -0.6,15.4 23.2,51.4 64.1,93.3l12.7,12.8c28.7,27.9 64.2,57.8 104.4,85.5 140.2,96.5 308.7,155.6 475.9,155.6 75.1,0 150,-11 222.1,-31.2l54.7,54.7c-85.3,27.7 -178.8,44.7 -276.8,44.7 -256.6,0 -483.8,-126.3 -612.9,-246l-13.2,-12.6c-12.9,-12.6 -24.6,-25 -35.2,-37.1l-10.1,-12a468.4,468.4 0,0 1,-4.8 -5.9l-8.9,-11.7C21.6,557.3 7.3,524.7 8.3,501.2c0.9,-21.5 13.8,-50.7 37,-83.5l8.6,-11.7 9.5,-12 5.1,-6.1 10.7,-12.4 5.6,-6.2 11.9,-12.6 12.6,-12.8c4.4,-4.3 8.8,-8.6 13.4,-12.9l14.1,-12.9c12.1,-10.8 24.9,-21.6 38.4,-32.3l16.6,-12.8a976.1,976.1 0,0 1,132.7 -84.2zM733.6,83c400.6,0 725.3,284.8 725.3,418.1 0,42.4 -32.9,100.2 -90.6,159.2l-12.8,12.7 -6.7,6.3 -13.9,12.7c-4.7,4.2 -9.6,8.4 -14.6,12.6l-15.3,12.6c-5.2,4.2 -10.5,8.3 -16,12.5l-16.6,12.4a929.4,929.4 0,0 1,-125.5 76.1l-51,-50.9a880.5,880.5 0,0 0,112.4 -63.8c58.2,-39 106.9,-82.3 140.3,-123.6 28.1,-34.7 41.9,-64.1 41.9,-78.7 0,-14.6 -13.8,-44 -41.8,-78.7 -33.4,-41.3 -82.1,-84.7 -140.3,-123.6 -136.9,-91.6 -305.5,-147.6 -474.9,-147.6 -74.3,0 -148.5,11.7 -220.2,33.2l-54.1,-54.2c84.4,-29.1 177,-47.2 274.4,-47.2zM536.7,400.8l52,52a152.7,152.7 0,0 0,193.2 193.2l52,52.1a220.9,220.9 0,0 1,-297.2 -297.3zM733.6,280.2a220.9,220.9 0,0 1,196.9 321.3L878.5,549.5a152.7,152.7 0,0 0,-193.2 -193.2L633.2,304.3a220,220 0,0 1,100.4 -24.1z"
android:fillColor="#bfbfbf"/> android:fillColor="#666666"/>
</vector> </vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M819.2,364.8h-44.8L774.4,128c0,-17.1 -14.9,-32 -32,-32L281.6,96c-17.1,0 -32,14.9 -32,32v236.8L204.8,364.8c-59.7,0 -108.8,49.1 -108.8,108.8v192c0,59.7 49.1,108.8 108.8,108.8h44.8L249.6,896c0,17.1 14.9,32 32,32h460.8c17.1,0 32,-14.9 32,-32v-121.6h44.8c59.7,0 108.8,-49.1 108.8,-108.8v-192c0,-59.7 -49.1,-108.8 -108.8,-108.8zM313.6,160h396.8v204.8L313.6,364.8L313.6,160zM710.4,864L313.6,864L313.6,620.8h396.8v243.2zM864,665.6c0,25.6 -19.2,44.8 -44.8,44.8h-44.8v-121.6c0,-17.1 -14.9,-32 -32,-32L281.6,556.8c-17.1,0 -32,14.9 -32,32v121.6L204.8,710.4c-25.6,0 -44.8,-19.2 -44.8,-44.8v-192c0,-25.6 19.2,-44.8 44.8,-44.8h614.4c25.6,0 44.8,19.2 44.8,44.8v192z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512,74.7C270.9,74.7 74.7,270.9 74.7,512S270.9,949.3 512,949.3 949.3,753.1 949.3,512 753.1,74.7 512,74.7zM512,885.3c-204.8,0 -373.3,-168.5 -373.3,-373.3S307.2,138.7 512,138.7 885.3,307.2 885.3,512 716.8,885.3 512,885.3z"
android:fillColor="#666666"/>
<path
android:pathData="M512,320m-42.7,0a42.7,42.7 0,1 0,85.3 0,42.7 42.7,0 1,0 -85.3,0Z"
android:fillColor="#666666"/>
<path
android:pathData="M512,437.3c-17.1,0 -32,14.9 -32,32v234.7c0,17.1 14.9,32 32,32s32,-14.9 32,-32V469.3c0,-17.1 -14.9,-32 -32,-32z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M853.3,138.7H170.7c-17.1,0 -32,14.9 -32,32v128c0,17.1 14.9,32 32,32s32,-14.9 32,-32V202.7h277.3v618.7H384c-17.1,0 -32,14.9 -32,32s14.9,32 32,32h256c17.1,0 32,-14.9 32,-32s-14.9,-32 -32,-32h-96v-618.7h277.3V298.7c0,17.1 14.9,32 32,32s32,-14.9 32,-32V170.7c0,-17.1 -14.9,-32 -32,-32z"
android:fillColor="#666666"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M874.7,544c-17.1,0 -32,14.9 -32,32v256c0,6.4 -4.3,10.7 -10.7,10.7H192c-6.4,0 -10.7,-4.3 -10.7,-10.7V192c0,-6.4 4.3,-10.7 10.7,-10.7h256c17.1,0 32,-14.9 32,-32s-14.9,-32 -32,-32H192C151.5,117.3 117.3,151.5 117.3,192v640c0,40.5 34.1,74.7 74.7,74.7h640c40.5,0 74.7,-34.1 74.7,-74.7V576c0,-17.1 -14.9,-32 -32,-32z"
android:fillColor="#666666"/>
<path
android:pathData="M874.7,117.3H640c-17.1,0 -32,14.9 -32,32s14.9,32 32,32h157.9L509.9,467.2c-12.8,12.8 -12.8,32 0,44.8 6.4,6.4 14.9,8.5 23.5,8.5s17.1,-2.1 23.5,-8.5l285.9,-285.9V384c0,17.1 14.9,32 32,32s32,-14.9 32,-32V149.3c0,-17.1 -14.9,-32 -32,-32z"
android:fillColor="#666666"/>
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="433dp" android:width="24dp"
android:height="256dp" android:height="14dp"
android:viewportWidth="1732" android:viewportWidth="1732"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M872.8,34.5c462.2,0 836.9,328.7 836.9,482.5 0,49 -37.9,115.6 -104.5,183.7l-14.7,14.6 -7.7,7.3 -16,14.6c-5.5,4.9 -11.1,9.7 -16.8,14.6l-17.6,14.5c-6,4.8 -12.2,9.6 -18.4,14.4l-19.2,14.3c-153.2,110.8 -375.1,204.5 -622,204.5 -296,0 -558.2,-145.7 -707.2,-283.9l-15.3,-14.5c-14.8,-14.5 -28.4,-28.8 -40.6,-42.8l-11.7,-13.9a540.4,540.4 0,0 1,-5.5 -6.9l-10.3,-13.5C51.2,581.7 34.8,544.1 35.8,516.9c1,-24.9 16,-58.4 42.7,-96.4l10,-13.5 10.9,-13.9c1.9,-2.4 3.8,-4.7 5.8,-7.1l12.3,-14.3 6.5,-7.2 13.7,-14.6 14.6,-14.7c5,-4.9 10.2,-9.9 15.5,-14.8l16.3,-14.9c13.9,-12.4 28.8,-24.9 44.3,-37.3l19.1,-14.8c152.8,-115.4 375.5,-219.1 625.2,-219.1zM872.8,113.2c-192.4,0 -384.2,67.9 -544.9,179.4a957,957 0,0 0,-107.4 86.7l-16.3,15.6c-5.2,5.2 -10.3,10.3 -15.1,15.4l-13.9,14.9a505.7,505.7 0,0 0,-33.8 41.2l-8.4,12.2c-11.5,17.6 -18,31.9 -18.4,41.5 -0.7,17.7 26.9,59.2 74,107.6l14.7,14.7c33.1,32.2 74,66.6 120.5,98.6 161.8,111.3 356.2,179.6 549.1,179.6 195.5,0 390.1,-64.5 548,-170.2 67.2,-45 123.4,-95 161.9,-142.7 32.3,-40 48.3,-74 48.3,-90.8 0,-16.8 -16,-50.7 -48.2,-90.8 -38.5,-47.7 -94.7,-97.7 -161.9,-142.7 -157.9,-105.7 -352.5,-170.3 -548,-170.3zM872.8,262a254.9,254.9 0,1 1,0 509.9,254.9 254.9,0 0,1 0,-509.9zM872.8,340.8a176.2,176.2 0,1 0,0 352.4,176.2 176.2,0 0,0 0,-352.4z" android:pathData="M872.8,34.5c462.2,0 836.9,328.7 836.9,482.5 0,49 -37.9,115.6 -104.5,183.7l-14.7,14.6 -7.7,7.3 -16,14.6c-5.5,4.9 -11.1,9.7 -16.8,14.6l-17.6,14.5c-6,4.8 -12.2,9.6 -18.4,14.4l-19.2,14.3c-153.2,110.8 -375.1,204.5 -622,204.5 -296,0 -558.2,-145.7 -707.2,-283.9l-15.3,-14.5c-14.8,-14.5 -28.4,-28.8 -40.6,-42.8l-11.7,-13.9a540.4,540.4 0,0 1,-5.5 -6.9l-10.3,-13.5C51.2,581.7 34.8,544.1 35.8,516.9c1,-24.9 16,-58.4 42.7,-96.4l10,-13.5 10.9,-13.9c1.9,-2.4 3.8,-4.7 5.8,-7.1l12.3,-14.3 6.5,-7.2 13.7,-14.6 14.6,-14.7c5,-4.9 10.2,-9.9 15.5,-14.8l16.3,-14.9c13.9,-12.4 28.8,-24.9 44.3,-37.3l19.1,-14.8c152.8,-115.4 375.5,-219.1 625.2,-219.1zM872.8,113.2c-192.4,0 -384.2,67.9 -544.9,179.4a957,957 0,0 0,-107.4 86.7l-16.3,15.6c-5.2,5.2 -10.3,10.3 -15.1,15.4l-13.9,14.9a505.7,505.7 0,0 0,-33.8 41.2l-8.4,12.2c-11.5,17.6 -18,31.9 -18.4,41.5 -0.7,17.7 26.9,59.2 74,107.6l14.7,14.7c33.1,32.2 74,66.6 120.5,98.6 161.8,111.3 356.2,179.6 549.1,179.6 195.5,0 390.1,-64.5 548,-170.2 67.2,-45 123.4,-95 161.9,-142.7 32.3,-40 48.3,-74 48.3,-90.8 0,-16.8 -16,-50.7 -48.2,-90.8 -38.5,-47.7 -94.7,-97.7 -161.9,-142.7 -157.9,-105.7 -352.5,-170.3 -548,-170.3zM872.8,262a254.9,254.9 0,1 1,0 509.9,254.9 254.9,0 0,1 0,-509.9zM872.8,340.8a176.2,176.2 0,1 0,0 352.4,176.2 176.2,0 0,0 0,-352.4z"
android:fillColor="#bfbfbf"/> android:fillColor="#666666"/>
</vector> </vector>

View File

@ -0,0 +1,293 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center">
<View
android:layout_width="32dp"
android:layout_height="4dp"
android:background="@drawable/dr_dialog_indicator_bg" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/dr_item_img_frame">
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher_round" />
<LinearLayout
android:id="@+id/lock_layout"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/dr_item_lock_bg"
android:gravity="center"
android:visibility="gone">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/lock" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/tvFileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:fontFamily="@font/poppins_medium"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/grey_text_color"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFileDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_regular"
android:text="@string/app_name"
android:textColor="@color/black_60"
android:textSize="14sp" />
<TextView
android:id="@+id/tvFileSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="@font/poppins_regular"
android:text="@string/app_name"
android:textColor="@color/black_60"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/collectBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center">
<ImageView
android:id="@+id/collectIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/collect" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/renameFileBtn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/rename_text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/poppins_regular"
android:text="@string/rename_file"
android:textColor="@color/grey_text_color"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/prompt" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/poppins_regular"
android:text="@string/file_details"
android:textColor="@color/grey_text_color"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/share" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/poppins_regular"
android:text="@string/share_file"
android:textColor="@color/grey_text_color"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/print" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/poppins_regular"
android:text="@string/print_pdf"
android:textColor="@color/grey_text_color"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/line_color" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/duplicate" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/poppins_medium"
android:text="@string/duplicate_file"
android:textColor="@color/grey_text_color"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/lock" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/poppins_medium"
android:text="@string/set_password"
android:textColor="@color/grey_text_color"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/file_delete" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/poppins_medium"
android:text="@string/delete_file"
android:textColor="@color/grey_text_color"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -38,7 +38,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/enter_password" android:hint="@string/enter_password"
app:endIconMode="password_toggle" app:endIconMode="password_toggle"
app:passwordToggleEnabled="true"> app:endIconDrawable="@drawable/dr_password_state">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword" android:id="@+id/etPassword"
@ -50,21 +50,6 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/showPasswordBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignEnd="@id/tilPassword"
android:layout_centerVertical="true"
android:gravity="center">
<ImageView
android:id="@+id/showPasswordIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/hide_password" />
</LinearLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -37,6 +37,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/enter_password" android:hint="@string/enter_password"
app:endIconDrawable="@drawable/dr_password_state"
app:endIconMode="password_toggle" app:endIconMode="password_toggle"
app:passwordToggleEnabled="true"> app:passwordToggleEnabled="true">
@ -50,22 +51,6 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/enterShowPasswordBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignEnd="@id/tilPassword"
android:layout_centerVertical="true"
android:gravity="center">
<ImageView
android:id="@+id/enterShowPasswordIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/hide_password" />
</LinearLayout>
</RelativeLayout> </RelativeLayout>
<RelativeLayout <RelativeLayout
@ -78,6 +63,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/confirm_password" android:hint="@string/confirm_password"
app:endIconDrawable="@drawable/dr_password_state"
app:endIconMode="password_toggle" app:endIconMode="password_toggle"
app:passwordToggleEnabled="true"> app:passwordToggleEnabled="true">
@ -91,22 +77,6 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/confirmShowPasswordBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignEnd="@id/tilConfirmPassword"
android:layout_centerVertical="true"
android:gravity="center">
<ImageView
android:id="@+id/confirmShowPasswordIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/hide_password" />
</LinearLayout>
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dr_rounded_corner_12_bg_white"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_semibold"
android:text="@string/rename_file"
android:textColor="@color/black"
android:textSize="20sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_name"
app:endIconDrawable="@drawable/delete"
app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_regular"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/tvCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:fontFamily="@font/poppins_regular"
android:padding="12dp"
android:text="@string/cancel"
android:textColor="@color/black_80" />
<TextView
android:id="@+id/tvConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_medium"
android:padding="12dp"
android:text="@string/ok"
android:textColor="@color/black" />
</LinearLayout>
</LinearLayout>

View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/dr_rounded_corner_12_bg_white"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView

View File

@ -13,4 +13,5 @@
<color name="bg_color">#F6F6F6</color> <color name="bg_color">#F6F6F6</color>
<color name="line_color">#E0E0E0</color> <color name="line_color">#E0E0E0</color>
<color name="black_img_color">#2c2c2c</color> <color name="black_img_color">#2c2c2c</color>
<color name="grey_text_color">#666666</color>
</resources> </resources>

View File

@ -39,4 +39,21 @@
<string name="pdf_loading_failed">PDF loading failed</string> <string name="pdf_loading_failed">PDF loading failed</string>
<string name="password_too_short">Password must be at least 4 characters</string> <string name="password_too_short">Password must be at least 4 characters</string>
<string name="password_not_match">Passwords do not match</string> <string name="password_not_match">Passwords do not match</string>
<string name="rename_file">Rename</string>
<string name="file_details">Details</string>
<string name="share_file">Share</string>
<string name="print_pdf">Print</string>
<string name="added_to_favorites">Added to Favorite</string>
<string name="removed_from_favorites">Removed from Favorites</string>
<string name="delete_file">Delete File</string>
<string name="set_password">Set Password</string>
<string name="remove_password">Remove Password</string>
<string name="duplicate_file">Duplicate File</string>
<string name="enter_name">Enter a name</string>
<string name="name_not_empty">File name cannot be empty</string>
<string name="name_not_changed">File name has not been changed</string>
<string name="name_invalid_chars">File name contains invalid characters: / \ : * ? " &lt; &gt; |</string>
<string name="name_too_long">File name is too long (max 255 characters)</string>
<string name="name_already_exists">A file with the same name already exists</string>
<string name="name_start_end_space">File name cannot start or end with space</string>
</resources> </resources>