封装扫描方法。
This commit is contained in:
parent
c642e3322e
commit
167b5574ec
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(./gradlew:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.all.pdfreader.pro.app.R
|
import com.all.pdfreader.pro.app.R
|
||||||
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
|
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
|
||||||
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
|
||||||
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.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
|
||||||
@ -20,15 +19,10 @@ import com.all.pdfreader.pro.app.model.SortDirection
|
|||||||
import com.all.pdfreader.pro.app.ui.dialog.SortDialogFragment
|
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.FileChangeObserver
|
||||||
import com.all.pdfreader.pro.app.util.FileUtils
|
import com.all.pdfreader.pro.app.util.PdfScanner
|
||||||
import com.all.pdfreader.pro.app.util.PdfMetadataExtractor
|
|
||||||
import com.all.pdfreader.pro.app.util.ScanManager
|
|
||||||
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
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback,
|
class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback,
|
||||||
PermissionDialogFragment.CloseCallback {
|
PermissionDialogFragment.CloseCallback {
|
||||||
@ -38,6 +32,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private val pdfRepository = getRepository()
|
private val pdfRepository = getRepository()
|
||||||
private lateinit var fileChangeObserver: FileChangeObserver
|
private lateinit var fileChangeObserver: FileChangeObserver
|
||||||
|
private lateinit var pdfScanner: PdfScanner
|
||||||
|
|
||||||
private val homeFragment = HomeFrag()
|
private val homeFragment = HomeFrag()
|
||||||
private val recentlyFragment = RecentlyFrag()
|
private val recentlyFragment = RecentlyFrag()
|
||||||
@ -56,6 +51,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
|
|
||||||
setupFragments()
|
setupFragments()
|
||||||
setupNavigation()
|
setupNavigation()
|
||||||
|
pdfScanner = PdfScanner(this, pdfRepository)
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
val restoredFragment =
|
val restoredFragment =
|
||||||
supportFragmentManager.getFragment(savedInstanceState, fragmentTag)
|
supportFragmentManager.getFragment(savedInstanceState, fragmentTag)
|
||||||
@ -70,16 +66,17 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
|
|
||||||
private fun scanningStrategy() {
|
private fun scanningStrategy() {
|
||||||
// 智能扫描策略
|
// 智能扫描策略
|
||||||
if (ScanManager.shouldScan(this)) {
|
if (pdfScanner.shouldScan()) {
|
||||||
logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
|
logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
|
||||||
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
||||||
scanAndLoadPdfFiles()
|
lifecycleScope.launch {
|
||||||
|
pdfScanner.scanAndLoadPdfFiles()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logDebug("❌ 权限不足,跳过扫描")
|
logDebug("❌ 权限不足,跳过扫描")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val lastScan = ScanManager.getLastScanTime(this)
|
val hoursAgo = pdfScanner.getHoursSinceLastScan()
|
||||||
val hoursAgo = TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
|
|
||||||
logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
|
logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,108 +150,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) {
|
|
||||||
if (!StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
|
||||||
logDebug("权限不足")
|
|
||||||
callback.invoke(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
logDebug("🔍 开始扫描PDF文件...")
|
|
||||||
|
|
||||||
// 扫描应用私有目录(无需权限)
|
|
||||||
val privateFiles = FileUtils.scanPdfFiles(this@MainActivity)
|
|
||||||
logDebug("📁 应用私有目录找到: ${privateFiles.size} 个PDF文件")
|
|
||||||
privateFiles.forEach { file ->
|
|
||||||
logDebug(" 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扫描MediaStore(需要权限)
|
|
||||||
val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(this@MainActivity)
|
|
||||||
logDebug("📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件")
|
|
||||||
mediaStoreFiles.forEach { file ->
|
|
||||||
logDebug(" 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并并去重
|
|
||||||
val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
|
|
||||||
logDebug("📊 总计扫描到: ${allFiles.size} 个PDF文件")
|
|
||||||
|
|
||||||
// 处理每个PDF文件
|
|
||||||
allFiles.forEachIndexed { index, file ->
|
|
||||||
logDebug("🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}")
|
|
||||||
|
|
||||||
if (FileUtils.isPdfFile(file)) {
|
|
||||||
val fileHash = FileUtils.calculateFileHash(file.absolutePath)
|
|
||||||
logDebug(" 🔑 文件哈希: $fileHash")
|
|
||||||
|
|
||||||
if (fileHash != null) {
|
|
||||||
val existingDoc = pdfRepository.getDocumentByHash(fileHash)
|
|
||||||
|
|
||||||
if (existingDoc == null) {
|
|
||||||
logDebug(" 🆕 发现新PDF文件: ${file.name}")
|
|
||||||
val metadata =
|
|
||||||
PdfMetadataExtractor.extractMetadata(file.absolutePath)
|
|
||||||
val document = PdfDocumentEntity(
|
|
||||||
fileHash = fileHash,
|
|
||||||
filePath = file.absolutePath,
|
|
||||||
fileName = file.name,
|
|
||||||
fileSize = file.length(),
|
|
||||||
lastModified = file.lastModified(),
|
|
||||||
pageCount = metadata?.pageCount ?: 0,
|
|
||||||
metadataTitle = metadata?.title,
|
|
||||||
metadataAuthor = metadata?.author,
|
|
||||||
metadataSubject = metadata?.subject,
|
|
||||||
metadataKeywords = metadata?.keywords,
|
|
||||||
metadataCreationDate = metadata?.creationDate?.time,
|
|
||||||
metadataModificationDate = metadata?.modificationDate?.time
|
|
||||||
)
|
|
||||||
pdfRepository.insertOrUpdateDocument(document)
|
|
||||||
logDebug(" ✅ 已保存到数据库: ${file.name}")
|
|
||||||
} else {
|
|
||||||
logDebug(" 📋 文件已存在: ${file.name}")
|
|
||||||
if (existingDoc.filePath != file.absolutePath) {
|
|
||||||
logDebug(" 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}")
|
|
||||||
val updatedDoc = existingDoc.copy(
|
|
||||||
filePath = file.absolutePath,
|
|
||||||
lastModified = file.lastModified()
|
|
||||||
)
|
|
||||||
pdfRepository.insertOrUpdateDocument(updatedDoc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印数据库中的总记录数
|
|
||||||
pdfRepository.getAllDocuments().collect { docs ->
|
|
||||||
logDebug("📊 数据库中共有: ${docs.size} 个PDF记录")
|
|
||||||
docs.forEach { doc ->
|
|
||||||
logDebug(
|
|
||||||
" 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
|
|
||||||
FileUtils.formatFileSize(
|
|
||||||
doc.fileSize
|
|
||||||
)
|
|
||||||
} - ${doc.thumbnailPath}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记扫描完成
|
|
||||||
ScanManager.markScanComplete(this@MainActivity)
|
|
||||||
val lastScanTime = ScanManager.getLastScanTime(this@MainActivity)
|
|
||||||
logDebug("✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}")
|
|
||||||
callback.invoke(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("❌ 扫描出错: ${e.message}", e)
|
|
||||||
callback.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ 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.ui.act.MainActivity
|
import com.all.pdfreader.pro.app.ui.act.MainActivity
|
||||||
import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
|
import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import com.all.pdfreader.pro.app.util.PdfScanner
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
|
class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
|
||||||
@ -48,7 +48,8 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
|
|||||||
// 下拉刷新示例
|
// 下拉刷新示例
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
(activity as? MainActivity)?.scanAndLoadPdfFiles { b ->
|
val pdfScanner = PdfScanner(requireContext(), getRepository())
|
||||||
|
pdfScanner.scanAndLoadPdfFiles { b ->
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/src/main/java/com/all/pdfreader/pro/app/util/LogUtil.kt
Normal file
22
app/src/main/java/com/all/pdfreader/pro/app/util/LogUtil.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
object LogUtil {
|
||||||
|
|
||||||
|
fun logDebug(tag: String, message: String) {
|
||||||
|
Log.d(tag, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logError(tag: String, message: String, throwable: Throwable? = null) {
|
||||||
|
Log.e(tag, message, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logInfo(tag: String, message: String) {
|
||||||
|
Log.i(tag, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logWarning(tag: String, message: String) {
|
||||||
|
Log.w(tag, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
127
app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt
Normal file
127
app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||||
|
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class PdfScanner(
|
||||||
|
private val context: Context,
|
||||||
|
private val pdfRepository: PdfRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) {
|
||||||
|
if (!StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 权限不足")
|
||||||
|
callback.invoke(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 🔍 开始扫描PDF文件...")
|
||||||
|
|
||||||
|
// 扫描应用私有目录(无需权限)
|
||||||
|
val privateFiles = FileUtils.scanPdfFiles(context)
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件")
|
||||||
|
privateFiles.forEach { file ->
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描MediaStore(需要权限)
|
||||||
|
val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context)
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件")
|
||||||
|
mediaStoreFiles.forEach { file ->
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并并去重
|
||||||
|
val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件")
|
||||||
|
|
||||||
|
// 处理每个PDF文件
|
||||||
|
allFiles.forEachIndexed { index, file ->
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}")
|
||||||
|
|
||||||
|
if (FileUtils.isPdfFile(file)) {
|
||||||
|
val fileHash = FileUtils.calculateFileHash(file.absolutePath)
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 🔑 文件哈希: $fileHash")
|
||||||
|
|
||||||
|
if (fileHash != null) {
|
||||||
|
val existingDoc = pdfRepository.getDocumentByHash(fileHash)
|
||||||
|
|
||||||
|
if (existingDoc == null) {
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 🆕 发现新PDF文件: ${file.name}")
|
||||||
|
val metadata =
|
||||||
|
PdfMetadataExtractor.extractMetadata(file.absolutePath)
|
||||||
|
val document = PdfDocumentEntity(
|
||||||
|
fileHash = fileHash,
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
fileName = file.name,
|
||||||
|
fileSize = file.length(),
|
||||||
|
lastModified = file.lastModified(),
|
||||||
|
pageCount = metadata?.pageCount ?: 0,
|
||||||
|
metadataTitle = metadata?.title,
|
||||||
|
metadataAuthor = metadata?.author,
|
||||||
|
metadataSubject = metadata?.subject,
|
||||||
|
metadataKeywords = metadata?.keywords,
|
||||||
|
metadataCreationDate = metadata?.creationDate?.time,
|
||||||
|
metadataModificationDate = metadata?.modificationDate?.time
|
||||||
|
)
|
||||||
|
pdfRepository.insertOrUpdateDocument(document)
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: ✅ 已保存到数据库: ${file.name}")
|
||||||
|
} else {
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📋 文件已存在: ${file.name}")
|
||||||
|
if (existingDoc.filePath != file.absolutePath) {
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}")
|
||||||
|
val updatedDoc = existingDoc.copy(
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
lastModified = file.lastModified()
|
||||||
|
)
|
||||||
|
pdfRepository.insertOrUpdateDocument(updatedDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印数据库中的总记录数
|
||||||
|
pdfRepository.getAllDocuments().collect { docs ->
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: 📊 数据库中共有: ${docs.size} 个PDF记录")
|
||||||
|
docs.forEach { doc ->
|
||||||
|
LogUtil.logDebug(
|
||||||
|
"ocean",
|
||||||
|
"PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${FileUtils.formatFileSize(doc.fileSize)} - ${doc.thumbnailPath}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记扫描完成
|
||||||
|
ScanManager.markScanComplete(context)
|
||||||
|
val lastScanTime = ScanManager.getLastScanTime(context)
|
||||||
|
LogUtil.logDebug("ocean", "PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}")
|
||||||
|
callback.invoke(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ocean", "PdfScanner: ❌ 扫描出错: ${e.message}", e)
|
||||||
|
callback.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldScan(): Boolean {
|
||||||
|
return ScanManager.shouldScan(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastScanTime(): Long {
|
||||||
|
return ScanManager.getLastScanTime(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHoursSinceLastScan(): Long {
|
||||||
|
val lastScan = getLastScanTime()
|
||||||
|
return TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,8 +4,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingStart="12dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="16dp"
|
||||||
tools:ignore="RtlSymmetry">
|
tools:ignore="RtlSymmetry">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -84,7 +84,8 @@
|
|||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
android:background="@color/line_color" />
|
android:background="@color/line_color" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user