diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1ed77ce..67dfffd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -55,4 +55,6 @@ dependencies {
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation ("com.google.android.material:material:1.13.0")
implementation(project(":pickerview"))
+ implementation ("androidx.media3:media3-exoplayer:1.8.0")
+ implementation ("androidx.media3:media3-ui:1.8.0")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 65ac6bf..13ce729 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.FileRecovery"
tools:targetApi="31">
+
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt
index 4196ef7..757f4c4 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt
@@ -20,13 +20,15 @@ abstract class BaseActivity : AppCompatActivity() {
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
- v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ if(addPadding()){
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ }
insets
}
initView()
initData()
}
-
+ protected open fun addPadding() = true
protected abstract fun inflateBinding(inflater: LayoutInflater): VB
protected open fun initView() {}
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt
index 3d45b83..b4f1d3b 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt
@@ -17,6 +17,10 @@ import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_photo
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_video
import kotlin.properties.Delegates
+
+/**
+ * 选择扫描所有文件还是扫描删除过的文件
+ */
class ScanSelectTypeActivity : BaseActivity() {
companion object {
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt
index d791f76..29f9742 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt
@@ -23,6 +23,9 @@ import org.jaaksi.pickerview.picker.TimePicker.OnTimeSelectListener
import org.jaaksi.pickerview.widget.DefaultCenterDecoration
import java.util.Date
+/**
+ * 自定义日期筛选,选择日期弹窗
+ */
class DatePickerDialogFragment(
var mContext: Context,
var title: String,
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt
index 20420bd..cb82f29 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
import com.ux.video.file.filerecovery.base.BaseAdapter
import com.ux.video.file.filerecovery.base.DiffBaseAdapter
import com.ux.video.file.filerecovery.databinding.PhotoDisplayDateAdapterBinding
@@ -16,6 +17,7 @@ import com.ux.video.file.filerecovery.utils.ScanRepository
class PhotoDisplayDateAdapter(
mContext: Context,
+ var scanType: Int,
var mColumns: Int,
var viewModel: ScanRepository,
var onSelectedUpdate: (resultPhotosFiles: ResultPhotosFiles, isAdd: Boolean) -> Unit,
@@ -34,14 +36,13 @@ class PhotoDisplayDateAdapter(
)
-
/**
* 返回所有嵌套的数据量总数
*/
fun getTotalChildCount(hideThumbnails: Boolean): Int {
- if(hideThumbnails){
+ if (hideThumbnails) {
return data.sumOf { it.second.filter { !it.isThumbnail }.size }
- }else{
+ } else {
return data.sumOf { it.second.size }
}
@@ -81,17 +82,18 @@ class PhotoDisplayDateAdapter(
val (date, files) = item
val childAdapter = PhotoDisplayDateChildAdapter(
mContext,
+ scanType,
mColumns,
viewModel,
{ resultPhotosFiles, addOrRemove, isDateAllSelected ->
//点击当前Adapter某一天的全选或者子Item上的选中都会回调到这里
tvDayAllSelect.isSelected = isDateAllSelected
onSelectedUpdate(resultPhotosFiles, addOrRemove)
- }, { updateHideThumbnails->
+ }, { updateHideThumbnails ->
tvDayAllSelect.isSelected = updateHideThumbnails
- },clickItem
+ }, clickItem
).apply { setData(files) }
allSelected?.let {
@@ -106,7 +108,15 @@ class PhotoDisplayDateAdapter(
textChildCounts.text = "(${files.size})"
recyclerChild.apply {
- layoutManager = GridLayoutManager(context, mColumns)
+ layoutManager = when (scanType) {
+ Common.VALUE_SCAN_TYPE_audio, Common.VALUE_SCAN_TYPE_deleted_audio -> {
+ LinearLayoutManager(context)
+ }
+ else -> {
+ GridLayoutManager(context, mColumns)
+ }
+ }
+
adapter = childAdapter
isNestedScrollingEnabled = false
}
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt
index 30b3bfd..8dd7577 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt
@@ -18,9 +18,11 @@ import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.ux.video.file.filerecovery.App
+import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.NewBaseAdapter
import com.ux.video.file.filerecovery.databinding.FileSpanCountThreeAdapterBinding
import com.ux.video.file.filerecovery.databinding.FileSpanCountTwoAdapterBinding
+import com.ux.video.file.filerecovery.databinding.OneAudioDocumentsItemBinding
import com.ux.video.file.filerecovery.utils.Common
import com.ux.video.file.filerecovery.utils.CustomTextView
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
@@ -29,6 +31,7 @@ import com.ux.video.file.filerecovery.utils.ScanRepository
class PhotoDisplayDateChildAdapter(
mContext: Context,
+ var scanType: Int,
var mColumns: Int,
var viewModel: ScanRepository,
/**
@@ -38,8 +41,8 @@ class PhotoDisplayDateChildAdapter(
* @param dateAllSelected 这组数据是否全部选中(某一天)
*/
var onSelectedUpdate: (resultPhotosFiles: ResultPhotosFiles, addOrRemove: Boolean, dateAllSelected: Boolean) -> Unit,
- var hideThumbnailsUpdate:(dateAllSelected: Boolean)-> Unit,
- var clickItem:(item:ResultPhotosFiles)-> Unit
+ var hideThumbnailsUpdate: (dateAllSelected: Boolean) -> Unit,
+ var clickItem: (item: ResultPhotosFiles) -> Unit
) :
NewBaseAdapter(mContext) {
@@ -49,10 +52,13 @@ class PhotoDisplayDateChildAdapter(
companion object {
+ //音频或者文档
+ private const val TYPE_ONE = 1
private const val TYPE_TWO = 2
private const val TYPE_THREE = 3
private const val TYPE_FOUR = 4
}
+
fun setAllSelected(isAdd: Boolean) {
data.forEach {
addOrRemove(it, isAdd)
@@ -61,12 +67,22 @@ class PhotoDisplayDateChildAdapter(
}
override fun getItemViewType(position: Int): Int {
- return when (mColumns) {
- 2 -> TYPE_TWO
- 3 -> TYPE_THREE
- 4 -> TYPE_FOUR
- else -> TYPE_THREE
+ when (scanType) {
+ Common.VALUE_SCAN_TYPE_audio, Common.VALUE_SCAN_TYPE_deleted_audio -> {
+ return TYPE_ONE
+ }
+
+ else -> {
+ return when (mColumns) {
+ 2 -> TYPE_TWO
+ 3 -> TYPE_THREE
+ 4 -> TYPE_FOUR
+ else -> TYPE_THREE
+ }
+ }
}
+
+
}
fun setColumns(int: Int) {
@@ -82,7 +98,7 @@ class PhotoDisplayDateChildAdapter(
val screenWidth = view.context.resources.displayMetrics.widthPixels
val i = (Common.itemSpacing).dpToPx(App.mAppContext) * (mColumns - 1)
val itemSize =
- (screenWidth - i - 2 * Common.horizontalSpacing.dpToPx(App.mAppContext) )/ mColumns
+ (screenWidth - i - 2 * Common.horizontalSpacing.dpToPx(App.mAppContext)) / mColumns
view.layoutParams = layoutParams.apply {
height = itemSize
}
@@ -94,6 +110,14 @@ class PhotoDisplayDateChildAdapter(
): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
+ TYPE_ONE -> OneHolder(
+ OneAudioDocumentsItemBinding.inflate(
+ inflater,
+ parent,
+ false
+ )
+ )
+
TYPE_TWO -> TwoHolder(
FileSpanCountTwoAdapterBinding.inflate(
inflater,
@@ -121,13 +145,34 @@ class PhotoDisplayDateChildAdapter(
when (holder) {
is TwoHolder -> holder.vb.run {
-
- initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item)
+ initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item, imageType)
}
is ThreeHolder -> holder.vb.run {
+ initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item, imageType)
+ }
- initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item)
+ is OneHolder -> {
+ item.run {
+ holder.vb.let {
+ it.textName.text = name
+ it.textDuration.text = Common.formatDuration(duration)
+ it.textSize.text = sizeString
+
+
+ viewModel.checkIsSelect(this).let { isSelected ->
+ it.imageSelect.isSelected = isSelected
+ addOrRemove(this, isSelected)
+ }
+ it.imageSelect.setOnClickListener {
+ it.isSelected = !it.isSelected
+ it.isSelected.let { newStatus ->
+ addOrRemove(this, newStatus)
+ }
+ }
+
+ }
+ }
}
}
@@ -139,15 +184,19 @@ class PhotoDisplayDateChildAdapter(
class TwoHolder(val vb: FileSpanCountTwoAdapterBinding) :
RecyclerView.ViewHolder(vb.root)
+ class OneHolder(val vb: OneAudioDocumentsItemBinding) :
+ RecyclerView.ViewHolder(vb.root)
private fun initDateView(
rootLayout: RelativeLayout,
imageSelectStatus: ImageView,
textSize: CustomTextView,
imageThumbnail: ImageView,
- item: ResultPhotosFiles
+ item: ResultPhotosFiles,
+ imageType: ImageView
) {
item.run {
+
viewModel.checkIsSelect(this).let {
imageSelectStatus.isSelected = it
addOrRemove(this, it)
@@ -160,6 +209,13 @@ class PhotoDisplayDateChildAdapter(
}
textSize.text = sizeString
+ imageType.setImageResource(
+ when (scanType) {
+ Common.VALUE_SCAN_TYPE_photo, Common.VALUE_SCAN_TYPE_deleted_photo -> R.drawable.icon_type_photo
+ Common.VALUE_SCAN_TYPE_video, Common.VALUE_SCAN_TYPE_deleted_video -> R.drawable.icon_type_video
+ else -> R.drawable.icon_type_photo
+ }
+ )
Glide.with(mContext)
.load(targetFile)
.apply(
@@ -211,14 +267,4 @@ class PhotoDisplayDateChildAdapter(
}
-
- fun getVisibleCount(list: MutableList = data, hideThumbnails: Boolean): Int {
- if(hideThumbnails){
- return list.filter { !it.isThumbnail }.size
- }else{
- return list.size
- }
-
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoInfoActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoInfoActivity.kt
index f767c95..9d25a72 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoInfoActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoInfoActivity.kt
@@ -3,12 +3,8 @@ package com.ux.video.file.filerecovery.photo
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
-import android.os.Bundle
import android.view.LayoutInflater
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
@@ -21,12 +17,19 @@ import com.bumptech.glide.request.target.Target
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.BaseActivity
import com.ux.video.file.filerecovery.databinding.ActivityPhotoInfoBinding
-import com.ux.video.file.filerecovery.databinding.ActivityPhotoSortingBinding
-import com.ux.video.file.filerecovery.photo.PhotoSortingActivity
import com.ux.video.file.filerecovery.success.RecoverySuccessActivity
import com.ux.video.file.filerecovery.utils.Common
+import com.ux.video.file.filerecovery.utils.Common.KEY_SCAN_TYPE
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_audio
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_audio
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_documents
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_photo
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_video
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_documents
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_photo
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_video
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
-import com.ux.video.file.filerecovery.utils.ScanManager
+import com.ux.video.file.filerecovery.video.VideoPlayActivity
class PhotoInfoActivity : BaseActivity() {
@@ -34,6 +37,7 @@ class PhotoInfoActivity : BaseActivity() {
val KEY_CLICK_ITEM = "click_item"
}
+ private var scanType: Int = VALUE_SCAN_TYPE_photo
private var myData: ResultPhotosFiles? = null
override fun inflateBinding(inflater: LayoutInflater): ActivityPhotoInfoBinding =
@@ -45,31 +49,34 @@ class PhotoInfoActivity : BaseActivity() {
intent.getParcelableExtra(KEY_CLICK_ITEM, ResultPhotosFiles::class.java)
} else {
@Suppress("DEPRECATION")
- intent.getParcelableExtra("MY_KEY")
+ intent.getParcelableExtra(KEY_CLICK_ITEM)
}
+ scanType = intent.getIntExtra(KEY_SCAN_TYPE, VALUE_SCAN_TYPE_photo)
+ setView()
+
+ }
+
+
+ override fun initData() {
+ super.initData()
binding.run {
imageViewBack.setOnClickListener { finish() }
- myData?.let { resultPhotosFiles->
+ myData?.let { resultPhotosFiles ->
tvName.text = resultPhotosFiles.name
tvPath.text = resultPhotosFiles.path
tvDate.text = Common.getFormatDate(resultPhotosFiles.lastModified)
tvResolution.text = resultPhotosFiles.resolution
+ tvDuration.text = Common.formatDuration(resultPhotosFiles.duration)
Glide.with(this@PhotoInfoActivity)
.load(resultPhotosFiles.targetFile)
- .apply(
- RequestOptions()
- .transform(
- CenterCrop(),
- RoundedCorners(8.dpToPx(this@PhotoInfoActivity))
- )
- )
+ .apply(RequestOptions().transform(CenterCrop(), RoundedCorners(8.dpToPx(this@PhotoInfoActivity))))
.listener(object : RequestListener {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
- target: com.bumptech.glide.request.target.Target,
+ target: Target,
isFirstResource: Boolean
): Boolean {
return false
@@ -92,8 +99,13 @@ class PhotoInfoActivity : BaseActivity() {
layoutBottom.tvLeft.run {
text = resources.getString(R.string.delete)
setOnClickListener {
- RecoverOrDeleteManager.showConfirmDeleteDialog(true,supportFragmentManager,lifecycleScope,setOf(resultPhotosFiles)){count->
- complete(count,1)
+ RecoverOrDeleteManager.showConfirmDeleteDialog(
+ true,
+ supportFragmentManager,
+ lifecycleScope,
+ setOf(resultPhotosFiles)
+ ) { count ->
+ complete(count, 1)
}
}
}
@@ -101,8 +113,12 @@ class PhotoInfoActivity : BaseActivity() {
layoutBottom.tvRight.run {
text = resources.getString(R.string.recover)
setOnClickListener {
- RecoverOrDeleteManager.showRecoveringDialog(supportFragmentManager,lifecycleScope,setOf(resultPhotosFiles)){count->
- complete(count,0)
+ RecoverOrDeleteManager.showRecoveringDialog(
+ supportFragmentManager,
+ lifecycleScope,
+ setOf(resultPhotosFiles)
+ ) { count ->
+ complete(count, 0)
}
}
}
@@ -110,11 +126,74 @@ class PhotoInfoActivity : BaseActivity() {
}
}
- private fun complete(number: Int,type: Int) {
+ private fun setView() {
+ binding.run {
+ when (scanType) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo -> {
+ layoutName.isVisible = true
+ layoutPath.isVisible = true
+ layoutResolution.isVisible = true
+ layoutDate.isVisible = true
+
+ layoutType.isVisible = false
+ layoutSize.isVisible = false
+ layoutDuration.isVisible = false
+
+ imPlay.isVisible = false
+ }
+
+ VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_deleted_video -> {
+ layoutName.isVisible = true
+ layoutPath.isVisible = true
+ layoutResolution.isVisible = true
+ layoutDate.isVisible = true
+ layoutDuration.isVisible = true
+
+ layoutType.isVisible = false
+ layoutSize.isVisible = false
+
+ imPlay.isVisible = true
+ myData?.let { data->
+ frameImage.setOnClickListener {
+ startActivity(Intent(this@PhotoInfoActivity, VideoPlayActivity::class.java).apply {
+ putExtra(VideoPlayActivity.KEY_DATA, data)
+ })
+ }
+ }
+ }
+
+ VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_deleted_audio -> {
+ layoutName.isVisible = true
+ layoutPath.isVisible = true
+ layoutSize.isVisible = true
+ layoutDate.isVisible = true
+ layoutDuration.isVisible = true
+
+ layoutResolution.isVisible = false
+ layoutType.isVisible = false
+ }
+
+ VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ layoutName.isVisible = true
+ layoutType.isVisible = true
+ layoutPath.isVisible = true
+ layoutSize.isVisible = true
+ layoutDate.isVisible = true
+
+ layoutDuration.isVisible = false
+ layoutDuration.isVisible = false
+ }
+ }
+
+ }
+
+ }
+
+ private fun complete(number: Int, type: Int) {
finish()
startActivity(Intent(this@PhotoInfoActivity, RecoverySuccessActivity::class.java).apply {
- putExtra(RecoverySuccessActivity.KEY_SUCCESS_COUNT,number)
- putExtra(RecoverySuccessActivity.KEY_SUCCESS_TYPE,type)
+ putExtra(RecoverySuccessActivity.KEY_SUCCESS_COUNT, number)
+ putExtra(RecoverySuccessActivity.KEY_SUCCESS_TYPE, type)
})
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt
index e5c5439..c77a4ec 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt
@@ -4,6 +4,7 @@ import android.content.Intent
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
+import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
@@ -13,8 +14,19 @@ import com.ux.video.file.filerecovery.base.BaseActivity
import com.ux.video.file.filerecovery.databinding.ActivityPhotoSortingBinding
import com.ux.video.file.filerecovery.success.RecoverySuccessActivity
import com.ux.video.file.filerecovery.utils.Common
+import com.ux.video.file.filerecovery.utils.Common.KEY_SCAN_TYPE
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_audio
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_audio
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_documents
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_photo
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_video
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_documents
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_photo
+import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_video
import com.ux.video.file.filerecovery.utils.Common.setItemSelect
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
+import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterByDuration
+import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterByDurationList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySize
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySizeList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterRemoveThumbnailsAsync
@@ -22,12 +34,11 @@ import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterThumbnailsAsyn
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRange
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRangeList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
+import com.ux.video.file.filerecovery.utils.ExtendFunctions.kbToBytes
import com.ux.video.file.filerecovery.utils.ExtendFunctions.mbToBytes
+import com.ux.video.file.filerecovery.utils.ExtendFunctions.minutesToMillisecond
import com.ux.video.file.filerecovery.utils.ExtendFunctions.removeItem
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
-import com.ux.video.file.filerecovery.utils.ScanManager
-import com.ux.video.file.filerecovery.utils.ScanManager.copySelectedFilesAsync
-import com.ux.video.file.filerecovery.utils.ScanManager.deleteFilesAsync
import com.ux.video.file.filerecovery.utils.ScanRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -57,6 +68,8 @@ class PhotoSortingActivity : BaseActivity() {
val SORT_DESC_DATE = 3
}
+ private var scanType: Int = VALUE_SCAN_TYPE_photo
+
private var sortDialogFragment: SortDialogFragment? = null
private var columns = 3
private var dateAdapter: PhotoDisplayDateAdapter? = null
@@ -76,7 +89,7 @@ class PhotoSortingActivity : BaseActivity() {
private var filterDate = FILTER_DATE_ALL
//筛选大小,默认全部-1
- private var filterSize = FILTER_SIZE_ALL
+ private var filterSize: String = "All"
private var filterDatePopupWindows: DateFilterPopupWindows? = null
private var filterStartDate: Date? = null
@@ -103,6 +116,8 @@ class PhotoSortingActivity : BaseActivity() {
private lateinit var mItemDecoration: GridSpacingItemDecoration
+ private lateinit var sizeFilterItemArray: Array
+
private lateinit var viewModel: ScanRepository
override fun inflateBinding(inflater: LayoutInflater): ActivityPhotoSortingBinding =
@@ -110,13 +125,12 @@ class PhotoSortingActivity : BaseActivity() {
override fun initData() {
super.initData()
-
+ scanType = intent.getIntExtra(KEY_SCAN_TYPE, VALUE_SCAN_TYPE_photo)
val list: ArrayList? =
intent.getParcelableArrayListExtraCompat(KEY_PHOTO_FOLDER_FILE)
mItemDecoration =
GridSpacingItemDecoration(columns, Common.itemSpacing, Common.horizontalSpacing)
updateButtonCounts(0)
-
viewModel = ViewModelProvider(this).get(ScanRepository::class.java)
viewModel.selectedLiveData.observe(this) { selectedSet ->
@@ -129,12 +143,11 @@ class PhotoSortingActivity : BaseActivity() {
Common.showLog("当前显示筛选数据 选中状态更新: ${displaySet.size}")
updateCurrentIsAllSelectStatus()
}
-
+ setScanTypeView()
list?.let {
binding.tvThumbnailCounts.text =
getString(R.string.hide_thumbnails, it.filter { it.isThumbnail }.size)
-
//降序(最近的在前面)
sortByDateReverse = Common.getSortByDayNewToOldInit(it)
//升序(时间最远的在前面)
@@ -144,6 +157,7 @@ class PhotoSortingActivity : BaseActivity() {
sizeSortAdapter = PhotoDisplayDateChildAdapter(
this@PhotoSortingActivity,
+ scanType,
columns, viewModel,
{ resultPhotosFiles, isAdd, allSelected ->
viewModel.toggleSelection(isAdd, resultPhotosFiles)
@@ -156,6 +170,7 @@ class PhotoSortingActivity : BaseActivity() {
this@PhotoSortingActivity,
PhotoInfoActivity::class.java
).apply {
+ putExtra(KEY_SCAN_TYPE,scanType)
putExtra(PhotoInfoActivity.KEY_CLICK_ITEM, item)
})
@@ -163,6 +178,7 @@ class PhotoSortingActivity : BaseActivity() {
dateAdapter =
PhotoDisplayDateAdapter(
this@PhotoSortingActivity,
+ scanType,
columns,
viewModel,
{ actionPath, isAdd ->
@@ -173,6 +189,7 @@ class PhotoSortingActivity : BaseActivity() {
this@PhotoSortingActivity,
PhotoInfoActivity::class.java
).apply {
+ putExtra(KEY_SCAN_TYPE,scanType)
putExtra(PhotoInfoActivity.KEY_CLICK_ITEM, item)
})
}.apply {
@@ -182,166 +199,209 @@ class PhotoSortingActivity : BaseActivity() {
setDateAdapter()
setSingleDelete()
setFilter()
- binding.run {
- imageViewBack.setOnClickListener { finish() }
- switchHideThumbnails.setOnCheckedChangeListener { _, isChecked ->
- when (recyclerView.adapter) {
- is PhotoDisplayDateAdapter -> {
- lifecycleScope.launch {
- dateAdapter?.run {
- initGetCurrentDateList().let { list ->
- val filterThumbnailsAsync =
- if (isChecked) list.filterThumbnailsAsync() else list
- setData(filterThumbnailsAsync)
- checkRefreshDisPlaySelected(list1 = filterThumbnailsAsync)
- }
+ setAllClick()
- }
- }
-
- }
-
- is PhotoDisplayDateChildAdapter -> {
- lifecycleScope.launch {
- sizeSortAdapter?.run {
- initGetCurrentSizeList().let {
- val filterThumbnailsAsync =
- if (isChecked) it.filterRemoveThumbnailsAsync() else it
- setData(filterThumbnailsAsync)
- checkRefreshDisPlaySelected(list2 = filterThumbnailsAsync)
- }
-
- }
- }
-
- }
- }
- updateCurrentIsAllSelectStatus()
-
- }
- tvRecover.setOnClickListener {
-// showRecoveringDialog()
- RecoverOrDeleteManager.showRecoveringDialog(
- supportFragmentManager,
- lifecycleScope,
- filterSelectedSetList
- ) { count ->
- complete(count, 0)
- }
-
- }
- tvDelete.setOnClickListener {
-// showConfirmDeleteDialog()
- RecoverOrDeleteManager.showConfirmDeleteDialog(
- fragmentManager = supportFragmentManager,
- scope = lifecycleScope,
- selectedSetList = filterSelectedSetList
- ) { count ->
- complete(count, 1)
- }
-
- }
- imSort.setOnClickListener {
- sortDialogFragment = sortDialogFragment ?: SortDialogFragment {
- when (it) {
- SORT_ASC_DATE -> {
- setDateAdapter()
- lifecycleScope.launch {
- initGetCurrentDateList().let {
- val filterThumbnailsAsync =
- if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
- val sortByDayOldToNew =
- Common.getSortByDayOldToNew(filterThumbnailsAsync)
- dateAdapter?.setData(sortByDayOldToNew)
- resetCurrentDateList(sortByDayOldToNew)
- }
- sortReverse = false
- }
-
- }
-
- SORT_DESC_DATE -> {
- setDateAdapter()
- lifecycleScope.launch {
- initGetCurrentDateList().let {
- val filterThumbnailsAsync =
- if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
- val sortByDayNewToOld =
- Common.getSortByDayNewToOld(filterThumbnailsAsync)
- dateAdapter?.setData(sortByDayNewToOld)
- resetCurrentDateList(sortByDayNewToOld)
- }
- sortReverse = true
- }
- }
-
- SORT_DESC_SIZE -> {
- setSizeAdapter()
- lifecycleScope.launch {
- initGetCurrentSizeList().let {
- val filterThumbnailsAsync =
- if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
- val sortBySizeBigToSmall =
- Common.getSortBySizeBigToSmall(filterThumbnailsAsync)
- sizeSortAdapter?.setData(sortBySizeBigToSmall)
- resetCurrentSizeList(sortBySizeBigToSmall)
- }
- sortReverse = true
- }
-
- }
-
- SORT_ASC_SIZE -> {
- setSizeAdapter()
- lifecycleScope.launch {
- initGetCurrentSizeList().let {
- val filterThumbnailsAsync =
- if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
- val sortBySizeSmallToBig =
- Common.getSortBySizeSmallToBig(filterThumbnailsAsync)
- sizeSortAdapter?.setData(sortBySizeSmallToBig)
- resetCurrentSizeList(sortBySizeSmallToBig)
- }
- sortReverse = false
- }
- }
- }
- }
- sortDialogFragment?.show(supportFragmentManager, "")
- }
-
- //全选按钮 只对当前显示的数据有效
- tvSelectAll.setOnClickListener {
- it.isSelected = !it.isSelected
- when (binding.recyclerView.adapter) {
- is PhotoDisplayDateAdapter -> {
- dateAdapter?.setAllSelected(it.isSelected)
- dateAdapter?.getCurrentData()?.let {
- it as List>>
-
- if (it.size > 0)
- Common.showLog("------------全选按钮 日期-${it.size} ${it[0].second[0].path}")
- }
-
- }
- is PhotoDisplayDateChildAdapter -> {
- sizeSortAdapter?.setAllSelected(it.isSelected)
- sizeSortAdapter?.getCurrentData()?.let {
- it as List
- if (it.size > 0)
- Common.showLog("------------全选按钮 大小-${it.size} ${it[0].path}")
- }
- }
- }
-
-
-
- }
- }
}
}
+ private fun setAllClick() {
+ binding.run {
+ imageViewBack.setOnClickListener { finish() }
+ switchHideThumbnails.setOnCheckedChangeListener { _, isChecked ->
+ when (recyclerView.adapter) {
+ is PhotoDisplayDateAdapter -> {
+ lifecycleScope.launch {
+ dateAdapter?.run {
+ initGetCurrentDateList().let { list ->
+ val filterThumbnailsAsync =
+ if (isChecked) list.filterThumbnailsAsync() else list
+ setData(filterThumbnailsAsync)
+ checkRefreshDisPlaySelected(list1 = filterThumbnailsAsync)
+ }
+
+ }
+ }
+
+ }
+
+ is PhotoDisplayDateChildAdapter -> {
+ lifecycleScope.launch {
+ sizeSortAdapter?.run {
+ initGetCurrentSizeList().let {
+ val filterThumbnailsAsync =
+ if (isChecked) it.filterRemoveThumbnailsAsync() else it
+ setData(filterThumbnailsAsync)
+ checkRefreshDisPlaySelected(list2 = filterThumbnailsAsync)
+ }
+
+ }
+ }
+
+ }
+ }
+ updateCurrentIsAllSelectStatus()
+
+ }
+ tvRecover.setOnClickListener {
+ RecoverOrDeleteManager.showRecoveringDialog(
+ supportFragmentManager,
+ lifecycleScope,
+ filterSelectedSetList
+ ) { count ->
+ complete(count, 0)
+ }
+
+ }
+ tvDelete.setOnClickListener {
+ RecoverOrDeleteManager.showConfirmDeleteDialog(
+ fragmentManager = supportFragmentManager,
+ scope = lifecycleScope,
+ selectedSetList = filterSelectedSetList
+ ) { count ->
+ complete(count, 1)
+ }
+
+ }
+ imSort.setOnClickListener {
+ sortDialogFragment = sortDialogFragment ?: SortDialogFragment {
+ when (it) {
+ SORT_ASC_DATE -> {
+ setDateAdapter()
+ lifecycleScope.launch {
+ initGetCurrentDateList().let {
+ val filterThumbnailsAsync =
+ if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
+ val sortByDayOldToNew =
+ Common.getSortByDayOldToNew(filterThumbnailsAsync)
+ dateAdapter?.setData(sortByDayOldToNew)
+ resetCurrentDateList(sortByDayOldToNew)
+ }
+ sortReverse = false
+ }
+
+ }
+
+ SORT_DESC_DATE -> {
+ setDateAdapter()
+ lifecycleScope.launch {
+ initGetCurrentDateList().let {
+ val filterThumbnailsAsync =
+ if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
+ val sortByDayNewToOld =
+ Common.getSortByDayNewToOld(filterThumbnailsAsync)
+ dateAdapter?.setData(sortByDayNewToOld)
+ resetCurrentDateList(sortByDayNewToOld)
+ }
+ sortReverse = true
+ }
+ }
+
+ SORT_DESC_SIZE -> {
+ setSizeAdapter()
+ lifecycleScope.launch {
+ initGetCurrentSizeList().let {
+ val filterThumbnailsAsync =
+ if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
+ val sortBySizeBigToSmall =
+ Common.getSortBySizeBigToSmall(filterThumbnailsAsync)
+ sizeSortAdapter?.setData(sortBySizeBigToSmall)
+ resetCurrentSizeList(sortBySizeBigToSmall)
+ }
+ sortReverse = true
+ }
+
+ }
+
+ SORT_ASC_SIZE -> {
+ setSizeAdapter()
+ lifecycleScope.launch {
+ initGetCurrentSizeList().let {
+ val filterThumbnailsAsync =
+ if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
+ val sortBySizeSmallToBig =
+ Common.getSortBySizeSmallToBig(filterThumbnailsAsync)
+ sizeSortAdapter?.setData(sortBySizeSmallToBig)
+ resetCurrentSizeList(sortBySizeSmallToBig)
+ }
+ sortReverse = false
+ }
+ }
+ }
+ }
+ sortDialogFragment?.show(supportFragmentManager, "")
+ }
+
+ //全选按钮 只对当前显示的数据有效
+ tvSelectAll.setOnClickListener {
+ it.isSelected = !it.isSelected
+ when (binding.recyclerView.adapter) {
+ is PhotoDisplayDateAdapter -> {
+ dateAdapter?.setAllSelected(it.isSelected)
+ dateAdapter?.getCurrentData()?.let {
+ it as List>>
+ if (it.size > 0)
+ Common.showLog("------------全选按钮 日期-${it.size} ${it[0].second[0].path}")
+ }
+
+ }
+
+ is PhotoDisplayDateChildAdapter -> {
+ sizeSortAdapter?.setAllSelected(it.isSelected)
+ sizeSortAdapter?.getCurrentData()?.let {
+ it as List
+ if (it.size > 0)
+ Common.showLog("------------全选按钮 大小-${it.size} ${it[0].path}")
+ }
+ }
+ }
+
+
+ }
+ }
+ }
+
+
+ /**
+ * 不同类型下的ui和功能点区分
+ */
+ private fun setScanTypeView() {
+ binding.run {
+ when (scanType) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo -> {
+ titleSize.text = getString(R.string.size)
+ filterLayoutLinearlayout.isVisible = true
+ relativeThumbnails.isVisible = true
+ sizeFilterItemArray = resources.getStringArray(R.array.filter_size_photo)
+ }
+
+ VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_deleted_video -> {
+ titleSize.text = getString(R.string.duration)
+ filterLayoutLinearlayout.isVisible = true
+ relativeThumbnails.isVisible = false
+ sizeFilterItemArray =
+ resources.getStringArray(R.array.filter_duration_video_audio)
+ }
+
+ VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_deleted_audio -> {
+ titleSize.text = getString(R.string.duration)
+ filterLayoutLinearlayout.isVisible = false
+ relativeThumbnails.isVisible = false
+ sizeFilterItemArray =
+ resources.getStringArray(R.array.filter_duration_video_audio)
+ }
+
+ VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ titleSize.text = getString(R.string.size)
+ filterLayoutLinearlayout.isVisible = false
+ relativeThumbnails.isVisible = false
+ sizeFilterItemArray = resources.getStringArray(R.array.filter_documents_size)
+ }
+ }
+
+ }
+ }
+
private fun updateCurrentIsAllSelectStatus() {
filterSelectedSetList.size.let {
binding.tvSelectCounts.text = it.toString()
@@ -498,19 +558,20 @@ class PhotoSortingActivity : BaseActivity() {
//大小筛选
filterSizeLayout.setOnClickListener {
setItemSelect(it as LinearLayout, true)
- resources.getStringArray(R.array.filter_size).let { data ->
+ sizeFilterItemArray.let { data ->
filterSizePopupWindows = filterSizePopupWindows ?: FilterPopupWindows(
this@PhotoSortingActivity,
data,
0,
{ clickValue ->
titleSize.text = clickValue
- when (clickValue) {
- data[0] -> filterSize = FILTER_SIZE_ALL
- data[1] -> filterSize = FILTER_SIZE_1
- data[2] -> filterSize = FILTER_SIZE_5
- data[3] -> filterSize = FILTER_SIZE_OVER_5
- }
+ filterSize = clickValue
+// when (clickValue) {
+// data[0] -> filterSize = clickValue
+// data[1] -> filterSize = FILTER_SIZE_1
+// data[2] -> filterSize = FILTER_SIZE_5
+// data[3] -> filterSize = FILTER_SIZE_OVER_5
+// }
startFilter()
}) {
setItemSelect(it, false)
@@ -578,48 +639,61 @@ class PhotoSortingActivity : BaseActivity() {
}
/**
- * 执行筛选结果
+ * 执行筛选结果 todo
*/
private fun startFilter() {
Common.showLog("--------------开始筛选")
+
+ val filterSizeCovert = filterSizeCovert(scanType, filterSize)
when (binding.recyclerView.adapter) {
//当前是时间排序
is PhotoDisplayDateAdapter -> {
//确定当前排序
val list = if (sortReverse) sortByDateReverse else sortedByDatePositive
- val filterSizeCovert = filterSizeCovert(filterSize)
list.filterWithinDateRange(
filterDate,
startDate = if (filterDate == FILTER_DATE_CUSTOMER) filterStartDate else null,
endDate = if (filterDate == FILTER_DATE_CUSTOMER) filterEndDate else null
- )
- .filterBySize(filterSizeCovert.first, filterSizeCovert.second)
- .let { currentList ->
- checkRefreshDisPlaySelected(list1 = currentList)
- dateAdapter?.resetAllValue(null)
- dateAdapter?.setData(currentList)
- resetCurrentDateList(currentList)
+ ).run {
+ when (scanType) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo, VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ filterBySize(filterSizeCovert.first, filterSizeCovert.second)
+ }
+ else -> {
+ filterByDuration(filterSizeCovert.first, filterSizeCovert.second)
+ }
}
+ }.let { currentList ->
+ checkRefreshDisPlaySelected(list1 = currentList)
+ dateAdapter?.resetAllValue(null)
+ dateAdapter?.setData(currentList)
+ resetCurrentDateList(currentList)
+ }
+
}
//当前是大小排序
is PhotoDisplayDateChildAdapter -> {
val list = if (sortReverse) sortBySizeBigToSmall else sortBySizeSmallToBig
- val filterSizeCovert = filterSizeCovert(filterSize)
list.filterWithinDateRangeList(
filterDate,
startDate = if (filterDate == FILTER_DATE_CUSTOMER) filterStartDate else null,
endDate = if (filterDate == FILTER_DATE_CUSTOMER) filterEndDate else null
- )
- .filterBySizeList(filterSizeCovert.first, filterSizeCovert.second)
- .let { currentList ->
- checkRefreshDisPlaySelected(list2 = currentList)
- sizeSortAdapter?.setData(currentList)
- resetCurrentSizeList(currentList)
-
-
+ ).run {
+ when (scanType) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo, VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ filterBySizeList(filterSizeCovert.first, filterSizeCovert.second)
+ }
+ else -> {
+ filterByDurationList(filterSizeCovert.first, filterSizeCovert.second)
+ }
}
+ }.let { currentList ->
+ checkRefreshDisPlaySelected(list2 = currentList)
+ sizeSortAdapter?.setData(currentList)
+ resetCurrentSizeList(currentList)
+ }
}
}
}
@@ -651,15 +725,44 @@ class PhotoSortingActivity : BaseActivity() {
}
- private fun filterSizeCovert(filterSize: Int): Pair {
- return when (filterSize) {
- FILTER_SIZE_ALL -> Pair(-1L, -1L)
- FILTER_SIZE_1 -> Pair(0L, 1.mbToBytes())
- FILTER_SIZE_5 -> Pair(1.mbToBytes(), 5.mbToBytes())
- FILTER_SIZE_OVER_5 -> Pair(5.mbToBytes(), Long.MAX_VALUE)
- else -> Pair(-1L, -1L)
+ private fun filterSizeCovert(scanType: Int, filterSize: String): Pair {
+ when (scanType) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo -> {
+ val stringArray = resources.getStringArray(R.array.filter_size_photo)
+ return when (filterSize) {
+ stringArray[0] -> Pair(-1L, -1L)
+ stringArray[1] -> Pair(0L, 1.mbToBytes())
+ stringArray[2] -> Pair(1.mbToBytes(), 5.mbToBytes())
+ stringArray[3] -> Pair(5.mbToBytes(), Long.MAX_VALUE)
+ else -> Pair(-1L, -1L)
+ }
+ }
+
+ VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_deleted_video, VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_deleted_audio -> {
+ val stringArray = resources.getStringArray(R.array.filter_duration_video_audio)
+ return when (filterSize) {
+ stringArray[0] -> Pair(-1L, -1L)
+ stringArray[1] -> Pair(0L, 5.minutesToMillisecond())
+ stringArray[2] -> Pair(5.minutesToMillisecond(), 20.minutesToMillisecond())
+ stringArray[3] -> Pair(20.minutesToMillisecond(), 60.minutesToMillisecond())
+ stringArray[4] -> Pair(60.minutesToMillisecond(), Long.MAX_VALUE)
+ else -> Pair(-1L, -1L)
+ }
+ }
+
+ VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ val stringArray = resources.getStringArray(R.array.filter_documents_size)
+ return when (filterSize) {
+ stringArray[0] -> Pair(-1L, -1L)
+ stringArray[1] -> Pair(0L, 500.kbToBytes())
+ stringArray[2] -> Pair(500.kbToBytes(), 1.mbToBytes())
+ stringArray[3] -> Pair(1.mbToBytes(), Long.MAX_VALUE)
+ else -> Pair(-1L, -1L)
+ }
+ }
}
+ return Pair(-1L, -1L)
}
/**
@@ -727,7 +830,6 @@ class PhotoSortingActivity : BaseActivity() {
}
-
/**
* 删除或者恢复完成
* @param type 0 恢复 1 删除
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt
index 79a9838..9e7ee9c 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt
@@ -3,6 +3,7 @@ package com.ux.video.file.filerecovery.photo
import java.io.File
import android.os.Parcelable
+import com.ux.video.file.filerecovery.utils.Common
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -17,7 +18,7 @@ data class ResultPhotosFiles(
val targetFile: File?
get() = path?.let { File(it) }
- //是否为缩略图文件(宽高任一小于 256)
+ //是否为缩略图文件(宽高任一小于 256)
val isThumbnail: Boolean
get() {
val parts = resolution.lowercase().split("*").mapNotNull {
@@ -29,4 +30,10 @@ data class ResultPhotosFiles(
}
return false
}
+
+
+ //音视频时长
+ val duration: Long
+ get() {
+ return Common.getMediaDuration(path.toString()) }
}
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt
index 2fec3f1..3537692 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt
@@ -2,16 +2,14 @@ package com.ux.video.file.filerecovery.result
import android.content.Intent
import android.view.LayoutInflater
-import android.view.View
import androidx.activity.OnBackPressedCallback
-import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.BaseActivity
import com.ux.video.file.filerecovery.databinding.ActivityScanResultDisplayBinding
import com.ux.video.file.filerecovery.photo.PhotoSortingActivity
import com.ux.video.file.filerecovery.photo.ResultPhotos
-import com.ux.video.file.filerecovery.result.ScanningActivity
+import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import com.ux.video.file.filerecovery.utils.Common.KEY_SCAN_TYPE
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_audio
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_deleted_audio
@@ -22,17 +20,16 @@ import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_documents
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_photo
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_video
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
-import com.ux.video.file.filerecovery.utils.ScanManager
-import com.ux.video.file.filerecovery.utils.ScanRepository
/**
* 扫描结果汇总展示
*/
class ScanResultDisplayActivity : BaseActivity() {
- private var scanResultAdapter: ScanResultAdapter? = null
private var scanType: Int = VALUE_SCAN_TYPE_photo
private var exitDialog: ExitDialogFragment? = null
+ private var list: ArrayList? = null
+
companion object {
val KEY_SCAN_RESULT = "scan_result"
@@ -43,50 +40,16 @@ class ScanResultDisplayActivity : BaseActivity
override fun initView() {
super.initView()
- val list: ArrayList? =
- intent.getParcelableArrayListExtraCompat(KEY_SCAN_RESULT)
- scanResultAdapter = ScanResultAdapter(this@ScanResultDisplayActivity) { folderLists ->
- startActivity(
- Intent(
- this@ScanResultDisplayActivity,
- PhotoSortingActivity::class.java
- ).apply {
- putParcelableArrayListExtra(
- PhotoSortingActivity.KEY_PHOTO_FOLDER_FILE,
- folderLists
- )
- })
- }
- binding.recyclerResult.run {
- adapter = scanResultAdapter
- layoutManager = LinearLayoutManager(this@ScanResultDisplayActivity)
- }
- list?.let {
- binding.run {
- textDirCount.text = it.size.toString()
- val sumOf = it.sumOf { it.allFiles.size }
- textAllCounts.text = sumOf.toString()
- }
- scanResultAdapter?.setData(it)
- }
+ list = intent.getParcelableArrayListExtraCompat(KEY_SCAN_RESULT)
+ scanType = intent.getIntExtra(KEY_SCAN_TYPE, VALUE_SCAN_TYPE_photo)
+ setSelectTypeTitle(scanType)
-// ScanRepository.instance.photoResults.observe(this@ScanResultDisplayActivity) {
-// binding.run {
-// textDirCount.text = it.size.toString()
-// val sumOf = it.sumOf { it.allFiles.size }
-// textAllCounts.text = sumOf.toString()
-// }
-// scanResultAdapter?.setData(it)
-// }
}
override fun initData() {
super.initData()
- scanType = intent.getIntExtra(KEY_SCAN_TYPE, VALUE_SCAN_TYPE_photo)
- setSelectTypeTitle(scanType)
binding.imageViewBack.setOnClickListener { dealExit() }
-
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
@@ -94,36 +57,96 @@ class ScanResultDisplayActivity : BaseActivity
dealExit()
}
})
+
+ binding.run {
+ val myAdapter = when (scanType) {
+ VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_deleted_audio, VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
+ bottomLayout.setBackgroundResource(R.drawable.bg_rectangle_white_top_20)
+ ScanResultDocumentsAdapter(
+ this@ScanResultDisplayActivity,
+ scanType
+ ) { folderLists ->
+ goSort(folderLists)
+ }
+ }
+
+ else -> {
+ bottomLayout.setBackgroundResource(0)
+ ScanResultPhotoAdapter(
+ this@ScanResultDisplayActivity,
+ scanType
+ ) { folderLists ->
+ goSort(folderLists)
+ }
+
+ }
+
+
+ }.apply {
+ list?.let {
+ textDirCount.text = it.size.toString()
+ val sumOf = it.sumOf { it.allFiles.size }
+ textAllCounts.text = sumOf.toString()
+ setData(it)
+ }
+ }
+ recyclerResult.run {
+ adapter = myAdapter
+ layoutManager = LinearLayoutManager(this@ScanResultDisplayActivity)
+ }
+
+
+ }
}
- private fun dealExit(){
- exitDialog = exitDialog?:ExitDialogFragment(){
+ private fun dealExit() {
+ exitDialog = exitDialog ?: ExitDialogFragment() {
finish()
}
- exitDialog?.show(supportFragmentManager,"")
+ exitDialog?.show(supportFragmentManager, "")
}
+
private fun setSelectTypeTitle(fileType: Int) {
binding.run {
when (fileType) {
VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo -> {
title.text = getString(R.string.photo_title)
+ textFileType.text = getString(R.string.text_photos)
}
VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_deleted_video -> {
title.text = getString(R.string.video_title)
+ textFileType.text = getString(R.string.text_videos)
}
VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_deleted_audio -> {
title.text = getString(R.string.audio_title)
+ textFileType.text = getString(R.string.text_audios)
}
VALUE_SCAN_TYPE_documents, VALUE_SCAN_TYPE_deleted_documents -> {
title.text = getString(R.string.document_title)
+ textFileType.text = getString(R.string.text_documents)
}
}
}
}
+
+
+ private fun goSort(list: ArrayList) {
+ startActivity(
+ Intent(
+ this@ScanResultDisplayActivity,
+ PhotoSortingActivity::class.java
+ ).apply {
+ putExtra(KEY_SCAN_TYPE, scanType)
+ putParcelableArrayListExtra(
+ PhotoSortingActivity.KEY_PHOTO_FOLDER_FILE,
+ list
+ )
+ })
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDocumentsAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDocumentsAdapter.kt
new file mode 100644
index 0000000..73d146b
--- /dev/null
+++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDocumentsAdapter.kt
@@ -0,0 +1,65 @@
+package com.ux.video.file.filerecovery.result
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.RequestOptions
+import com.ux.video.file.filerecovery.R
+import com.ux.video.file.filerecovery.base.BaseAdapter
+import com.ux.video.file.filerecovery.databinding.ScanResultAdapterBinding
+import com.ux.video.file.filerecovery.databinding.ScanResultDocumentsAdapterBinding
+import com.ux.video.file.filerecovery.photo.ResultPhotos
+import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
+import com.ux.video.file.filerecovery.utils.Common
+import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
+import java.io.File
+
+/**
+ * 文件或者音频的扫描结果汇总适配器
+ */
+class ScanResultDocumentsAdapter(
+ mContext: Context,
+ var type: Int,
+ var onClickItem: (allFiles: ArrayList) -> Unit
+) :
+ BaseAdapter(mContext) {
+ override fun getViewBinding(parent: ViewGroup): ScanResultDocumentsAdapterBinding =
+ ScanResultDocumentsAdapterBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+
+ @SuppressLint("SetTextI18n")
+ override fun bindItem(
+ holder: VHolder,
+ item: ResultPhotos
+ ) {
+
+ holder.vb.run {
+ item.run {
+ relativeLayout.setOnClickListener { onClickItem(allFiles) }
+ textDirName.text = dirName
+ textFileCounts.text = allFiles.size.toString()
+ when(type){
+ Common.VALUE_SCAN_TYPE_audio, Common.VALUE_SCAN_TYPE_deleted_audio->{
+ icon.setImageResource(R.drawable.icon_folder_audio)
+ }
+ Common.VALUE_SCAN_TYPE_documents, Common.VALUE_SCAN_TYPE_deleted_documents->{
+ icon.setImageResource(R.drawable.icon_folder_documents)
+ }
+ }
+
+ }
+ }
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultPhotoAdapter.kt
similarity index 98%
rename from app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt
rename to app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultPhotoAdapter.kt
index 3951f76..6f6adc5 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultPhotoAdapter.kt
@@ -16,8 +16,9 @@ import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
import java.io.File
-class ScanResultAdapter(
+class ScanResultPhotoAdapter(
mContext: Context,
+ var type: Int,
var onClickItem: (allFiles: ArrayList) -> Unit
) :
BaseAdapter(mContext) {
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt
index 09595f4..3f29c7d 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt
@@ -1,8 +1,11 @@
package com.ux.video.file.filerecovery.result
+import android.annotation.SuppressLint
import android.content.Intent
import android.os.Environment
import android.view.LayoutInflater
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.BaseActivity
@@ -26,20 +29,10 @@ import com.ux.video.file.filerecovery.utils.ScanState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
+import androidx.lifecycle.repeatOnLifecycle
class ScanningActivity : BaseActivity() {
- companion object {
-// val KEY_SCAN_TYPE = "scan_type"
-// val VALUE_SCAN_TYPE_photo = 0
-// val VALUE_SCAN_TYPE_deleted_photo = 1
-// val VALUE_SCAN_TYPE_video = 2
-// val VALUE_SCAN_TYPE_deleted_video = 3
-// val VALUE_SCAN_TYPE_audio = 4
-// val VALUE_SCAN_TYPE_deleted_audio = 5
-// val VALUE_SCAN_TYPE_documents = 6
-// val VALUE_SCAN_TYPE_deleted_documents = 7
- }
private var scanType: Int = VALUE_SCAN_TYPE_photo
override fun inflateBinding(inflater: LayoutInflater): ActivityScanningBinding =
@@ -53,7 +46,7 @@ class ScanningActivity : BaseActivity() {
VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_documents -> scanAll()
VALUE_SCAN_TYPE_deleted_photo, VALUE_SCAN_TYPE_deleted_video, VALUE_SCAN_TYPE_deleted_audio, VALUE_SCAN_TYPE_deleted_documents -> scanDeleted()
}
- binding.scanProgress.setCenterImage(R.drawable.im_photo_center_image)
+
binding.imageViewBack.setOnClickListener { finish() }
}
@@ -65,41 +58,49 @@ class ScanningActivity : BaseActivity() {
VALUE_SCAN_TYPE_photo -> {
title.text = getString(R.string.photo_title)
tvScanDescribe.text = getString(R.string.describe_photos)
+ scanProgress.setCenterImage(R.drawable.im_photo_center_image)
}
VALUE_SCAN_TYPE_deleted_photo -> {
title.text = getString(R.string.photo_title)
tvScanDescribe.text = getString(R.string.describe_delete_photos)
+ scanProgress.setCenterImage(R.drawable.im_photo_center_image)
}
VALUE_SCAN_TYPE_video -> {
title.text = getString(R.string.video_title)
tvScanDescribe.text = getString(R.string.describe_videos)
+ scanProgress.setCenterImage(R.drawable.im_video_center_image)
}
VALUE_SCAN_TYPE_deleted_video -> {
title.text = getString(R.string.video_title)
tvScanDescribe.text = getString(R.string.describe_delete_videos)
+ scanProgress.setCenterImage(R.drawable.im_video_center_image)
}
VALUE_SCAN_TYPE_audio -> {
title.text = getString(R.string.audio_title)
tvScanDescribe.text = getString(R.string.describe_audios)
+ scanProgress.setCenterImage(R.drawable.im_audio_center_image)
}
VALUE_SCAN_TYPE_deleted_audio -> {
title.text = getString(R.string.audio_title)
tvScanDescribe.text = getString(R.string.describe_delete_audios)
+ scanProgress.setCenterImage(R.drawable.im_audio_center_image)
}
VALUE_SCAN_TYPE_documents -> {
title.text = getString(R.string.document_title)
tvScanDescribe.text = getString(R.string.describe_documents)
+ scanProgress.setCenterImage(R.drawable.im_documents_center_image)
}
VALUE_SCAN_TYPE_deleted_documents -> {
title.text = getString(R.string.document_title)
tvScanDescribe.text = getString(R.string.describe_delete_documents)
+ scanProgress.setCenterImage(R.drawable.im_documents_center_image)
}
}
}
@@ -110,15 +111,18 @@ class ScanningActivity : BaseActivity() {
private fun scanAll() {
val total = 800
lifecycleScope.launch {
- val root = Environment.getExternalStorageDirectory()
- ScanManager.scanAllDocuments(this@ScanningActivity,root, type = scanType).flowOn(Dispatchers.IO).collect {
- when (it) {
- is ScanState.Progress -> {
- updateProgress(it)
- }
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ val root = Environment.getExternalStorageDirectory()
+ ScanManager.scanAllDocuments(this@ScanningActivity, root, type = scanType)
+ .flowOn(Dispatchers.IO).collect {
+ when (it) {
+ is ScanState.Progress -> {
+ updateProgress(it)
+ }
- is ScanState.Complete -> {
- updateComplete(it)
+ is ScanState.Complete -> {
+ updateComplete(it)
+ }
}
}
}
@@ -129,20 +133,23 @@ class ScanningActivity : BaseActivity() {
private fun scanDeleted() {
lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ val root = Environment.getExternalStorageDirectory()
+ ScanManager.scanHiddenPhotoAsync(this@ScanningActivity, root, type = scanType)
+ .flowOn(Dispatchers.IO).collect {
+ when (it) {
+ is ScanState.Progress -> {
+ updateProgress(it)
- val root = Environment.getExternalStorageDirectory()
- ScanManager.scanHiddenPhotoAsync(this@ScanningActivity,root, type = scanType).flowOn(Dispatchers.IO).collect {
- when (it) {
- is ScanState.Progress -> {
- updateProgress(it)
+ }
- }
-
- is ScanState.Complete -> {
- updateComplete(it)
+ is ScanState.Complete -> {
+ updateComplete(it)
+ }
}
}
}
+
}
}
@@ -166,27 +173,32 @@ class ScanningActivity : BaseActivity() {
}
+ @SuppressLint("SetTextI18n")
private fun updateComplete(scanState: ScanState.Complete) {
- binding.scanProgress.setProgress(100)
- scanState.let {
- startActivity(
- Intent(
- this@ScanningActivity,
- ScanResultDisplayActivity::class.java
- ).apply {
- putParcelableArrayListExtra(
- ScanResultDisplayActivity.KEY_SCAN_RESULT,
- it.result
- )
- })
- ScanManager.showLog(
- "HiddenScan",
- "完成: ${it.result.size}"
- )
-
+ binding.run {
+ scanProgress.setProgress(100)
+ scanState.let {
+ val size = it.result.size
+ if (size == 0) {
+ tvScanDescribe.text.let {
+ tvEmptyTypeFile.text = "0 $it"
+ tvSorry.text = getString(R.string.not_found,it)
+ }
+ relativeScanFinishedEmpty.isVisible = true
+ linearCounts.isVisible = false
+ }else{
+ finish()
+ startActivity(Intent(this@ScanningActivity, ScanResultDisplayActivity::class.java).apply {
+ putParcelableArrayListExtra(
+ ScanResultDisplayActivity.KEY_SCAN_RESULT,
+ it.result
+ )
+ putExtra(KEY_SCAN_TYPE, scanType)
+ })
+ }
+ ScanManager.showLog("HiddenScan", "完成: ${it.result.size}")
+ }
}
- finish()
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt
index 673230b..a927d39 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt
@@ -1,8 +1,10 @@
package com.ux.video.file.filerecovery.utils
+import android.annotation.SuppressLint
import android.content.Context
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
+import android.media.MediaMetadataRetriever
import android.os.Environment
import android.util.Log
import android.view.View
@@ -265,6 +267,36 @@ object Common {
}
+
+ fun getMediaDuration(filePath: String): Long {
+ val retriever = MediaMetadataRetriever()
+ return try {
+ retriever.setDataSource(filePath)
+ val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
+ durationStr?.toLongOrNull() ?: 0L // 单位:毫秒
+ } catch (e: Exception) {
+ e.printStackTrace()
+ 0L
+ } finally {
+ retriever.release()
+ }
+ }
+
+
+ fun formatDuration(ms: Long): String {
+ val totalSeconds = ms / 1000
+ val hours = totalSeconds / 3600
+ val minutes = (totalSeconds % 3600) / 60
+ val seconds = totalSeconds % 60
+
+ return if (hours > 0) {
+ String.format("%02d:%02d:%02d", hours, minutes, seconds)
+ } else {
+ String.format("%02d:%02d", minutes, seconds)
+ }
+ }
+
+
fun getFormatDate(time: Long): String {
return dateFormat.format(Date(time))
}
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt
index cb07409..5a60590 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt
@@ -40,21 +40,6 @@ object ExtendFunctions {
}
- /**
- * 按时间筛选:最近 N 个月
- */
-// fun List.filterWithinMonthsList(months: Int): List {
-// if (months == -1) return this
-// val today = Calendar.getInstance()
-// val monthsAgo = Calendar.getInstance().apply {
-// add(Calendar.MONTH, -months)
-// }
-// return this.filter {
-// val cal = Calendar.getInstance().apply { timeInMillis = it.lastModified }
-// !cal.before(monthsAgo) && !cal.after(today)
-// }
-// }
-
fun List.filterWithinDateRangeList(
months: Int = -1,
startDate: Date? = null,
@@ -64,10 +49,9 @@ object ExtendFunctions {
val today = Calendar.getInstance()
return when {
- // ✅ 1. -1 表示不过滤,返回全部
+
months == -1 -> this
- // ✅ 2. 0 表示仅根据 startDate / endDate 筛选
months == 0 -> this.filter { file ->
val date = Date(file.lastModified)
when {
@@ -78,7 +62,6 @@ object ExtendFunctions {
}
}
- // ✅ 3. 其他情况:按“最近 N 个月”筛选
else -> {
val monthsAgo = Calendar.getInstance().apply {
add(Calendar.MONTH, -months)
@@ -95,32 +78,7 @@ object ExtendFunctions {
- /**
- * 按文件大小筛选:区间 [minSize, maxSize]
- */
- fun List.filterBySizeList(
- minSize: Long,
- maxSize: Long
- ): List {
- if (minSize == -1L) return this
- return this.filter { it.size in minSize..maxSize }
- }
- /**
- * 按时间筛选:最近 N 个月
- */
-// fun List>>.filterWithinMonths(months: Int): List>> {
-// if (months == -1) return this
-// val sdf = Common.dateFormat
-// val today = Calendar.getInstance()
-// val monthsAgo = Calendar.getInstance().apply {
-// add(Calendar.MONTH, -months)
-// }
-// return this.filter { (dayStr, _) ->
-// val day = sdf.parse(dayStr)
-// day != null && !day.before(monthsAgo.time) && !day.after(today.time)
-// }
-// }
fun List>>.filterWithinDateRange(
months: Int = -1,
startDate: Date? = null,
@@ -136,10 +94,10 @@ object ExtendFunctions {
}
return when {
- // -1 表示不过滤,返回全部
+
months == -1 -> this
- // 0 表示只用日期范围过滤
+
months == 0 -> this.filter { (dayStr, _) ->
val day = sdf.parse(dayStr) ?: return@filter false
when {
@@ -150,22 +108,37 @@ object ExtendFunctions {
}
}
- // 其他情况:按“最近 N 个月”过滤
+
else -> this.filter { (dayStr, _) ->
val day = sdf.parse(dayStr) ?: return@filter false
!day.before(monthsAgo.time) && !day.after(today.time)
}
}
}
-
-
-
-
-
-
+ /**
+ * 按文件大小筛选:区间 [minSize, maxSize]
+ */
+ fun List.filterBySizeList(
+ minSize: Long,
+ maxSize: Long
+ ): List {
+ if (minSize == -1L) return this
+ return this.filter { it.size in minSize..maxSize }
+ }
/**
- * 分组数据:按大小筛选
+ * 按文件大小筛选:区间 [minSize, maxSize]
+ */
+ fun List.filterByDurationList(
+ minSize: Long,
+ maxSize: Long
+ ): List {
+ if (minSize == -1L) return this
+ return this.filter { it.duration in minSize..maxSize }
+ }
+
+ /**
+ * 分组数据:按大小筛选 ,图片和文件筛选文件大小
*/
fun List>>.filterBySize(
minSize: Long,
@@ -177,10 +150,34 @@ object ExtendFunctions {
if (filtered.isNotEmpty()) date to filtered else null
}
}
+
+ /**
+ * 分组数据:按大小筛选 ,音视频筛选时长
+ */
+ fun List>>.filterByDuration(
+ minSize: Long,
+ maxSize: Long
+ ): List>> {
+ if (minSize == -1L) return this
+ return this.mapNotNull { (date, files) ->
+ val filtered = files.filter { it.duration in minSize..maxSize }
+ if (filtered.isNotEmpty()) date to filtered else null
+ }
+ }
+
+
fun Int.mbToBytes(): Long {
return this * 1000L * 1000L
}
+ fun Int.kbToBytes(): Long {
+ return this * 1000L
+ }
+
+ fun Int.minutesToMillisecond(): Long {
+ return this * 60 * 1000L
+ }
+
/**
* 移除掉缩略图后的数据
*/
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt
index d4eb704..8e7dc8b 100644
--- a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt
+++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt
@@ -3,6 +3,7 @@ package com.ux.video.file.filerecovery.utils
import android.content.Context
import android.graphics.BitmapFactory
+import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.os.Environment
@@ -22,6 +23,8 @@ import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_photo
import com.ux.video.file.filerecovery.utils.Common.VALUE_SCAN_TYPE_video
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
@@ -63,9 +66,12 @@ object ScanManager {
val result = mutableMapOf>()
var fileCount = 0
suspend fun scanDocuments(dir: File, depth: Int) {
+ val context = currentCoroutineContext()
+
if (!dir.exists() || !dir.isDirectory) return
if (depth > maxDepth || fileCount >= maxFiles) return
dir.listFiles()?.forEach { file ->
+ context.ensureActive()
if (file.isDirectory) {
scanDocuments(file, depth + 1)
} else {
@@ -107,11 +113,12 @@ object ScanManager {
name = file.name,
path = file.absolutePath,
size = file.length(),
- sizeString = android.text.format.Formatter.formatFileSize(context, file.length()),
+ sizeString = android.text.format.Formatter.formatFileSize(
+ context,
+ file.length()
+ ),
lastModified = file.lastModified(),
- resolution = getImageSize(file).run {
- "$first*$second"
- }
+ resolution = getResolution(type,file)
)
}
ResultPhotos(dir, ArrayList(resultPhotosFilesList))
@@ -128,6 +135,25 @@ object ScanManager {
return Pair(width, height)
}
+
+ fun getVideoResolution(filePath: String): Pair {
+ val retriever = MediaMetadataRetriever()
+ return try {
+ retriever.setDataSource(filePath)
+ val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
+ ?.toIntOrNull() ?: 0
+ val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
+ ?.toIntOrNull() ?: 0
+ width to height
+ } catch (e: Exception) {
+ e.printStackTrace()
+ 0 to 0
+ } finally {
+ retriever.release()
+ }
+ }
+
+
/**
* 递归扫描隐藏目录下的有效图片(删除的图片)
* @param maxDepth // 最大递归深度
@@ -141,6 +167,7 @@ object ScanManager {
val result = mutableMapOf>()
var fileCount = 0
+
@RequiresApi(Build.VERSION_CODES.R)
suspend fun scanDir(dir: File, depth: Int, insideHidden: Boolean = false) {
if (!dir.exists() || !dir.isDirectory) return
@@ -192,11 +219,12 @@ object ScanManager {
name = file.name,
path = file.absolutePath,
size = file.length(),
- sizeString = android.text.format.Formatter.formatFileSize(context, file.length()),
+ sizeString = android.text.format.Formatter.formatFileSize(
+ context,
+ file.length()
+ ),
lastModified = file.lastModified(),
- resolution = getImageSize(file).run {
- "$first*$second"
- }
+ resolution = getResolution(type,file)
)
}
@@ -217,6 +245,22 @@ object ScanManager {
return file.length() // fallback
}
+ private fun getResolution(type: Int,file: File): String {
+ return when (type) {
+ VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_deleted_photo -> {
+ getImageSize(file).run {
+ "$first*$second"
+ }
+ }
+
+ VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_deleted_video -> getVideoResolution(file.path).run {
+ "$first*$second"
+ }
+
+ else -> ""
+ }
+ }
+
private fun isFormatFile(file: File, types: List): Boolean {
val ext = file.extension.lowercase()
return types.contains(ext)
diff --git a/app/src/main/java/com/ux/video/file/filerecovery/video/VideoPlayActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/video/VideoPlayActivity.kt
new file mode 100644
index 0000000..7231b0f
--- /dev/null
+++ b/app/src/main/java/com/ux/video/file/filerecovery/video/VideoPlayActivity.kt
@@ -0,0 +1,183 @@
+package com.ux.video.file.filerecovery.video
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.widget.SeekBar
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import com.ux.video.file.filerecovery.R
+import com.ux.video.file.filerecovery.base.BaseActivity
+import com.ux.video.file.filerecovery.databinding.ActivityScanSelectTypeBinding
+import com.ux.video.file.filerecovery.databinding.ActivityVideoPlayBinding
+import com.ux.video.file.filerecovery.photo.PhotoInfoActivity
+import com.ux.video.file.filerecovery.photo.PhotoInfoActivity.Companion.KEY_CLICK_ITEM
+import com.ux.video.file.filerecovery.photo.RecoverOrDeleteManager
+import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
+import com.ux.video.file.filerecovery.success.RecoverySuccessActivity
+import com.ux.video.file.filerecovery.utils.Common
+
+class VideoPlayActivity : BaseActivity() {
+
+ companion object {
+ val KEY_DATA = "key_data"
+ }
+
+ private lateinit var player: ExoPlayer
+ private var myData: ResultPhotosFiles? = null
+ private val updateHandler = Handler(Looper.getMainLooper())
+ override fun inflateBinding(inflater: LayoutInflater): ActivityVideoPlayBinding =
+ ActivityVideoPlayBinding.inflate(inflater)
+
+ override fun initView() {
+ super.initView()
+ }
+
+ override fun addPadding(): Boolean = false
+
+ override fun initData() {
+ super.initData()
+ myData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(KEY_DATA, ResultPhotosFiles::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(KEY_DATA)
+ }
+ initPlayer()
+ binding.run {
+ myData?.let { resultPhotosFiles->
+ imageBack.setOnClickListener { finish() }
+ playImage.setOnClickListener {
+ if (player.playbackState == Player.STATE_ENDED) {
+ player.seekTo(0)
+ }
+ if (!player.isPlaying) {
+ player.play()
+ it.isSelected = true
+ } else {
+ player.pause()
+ it.isSelected = false
+ }
+ }
+ seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(
+ seekBar: SeekBar?,
+ progress: Int,
+ fromUser: Boolean
+ ) {
+ if (fromUser) {
+ val newPosition = progress * player.duration / 100
+ player.seekTo(newPosition)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {
+
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+
+ }
+
+ })
+ startProgressUpdater()
+ layoutBottom.tvLeft.run {
+ text = resources.getString(R.string.delete)
+ setOnClickListener {
+ RecoverOrDeleteManager.showConfirmDeleteDialog(
+ true,
+ supportFragmentManager,
+ lifecycleScope,
+ setOf(resultPhotosFiles)
+ ) { count ->
+ complete(count, 1)
+ }
+ }
+ }
+
+ layoutBottom.tvRight.run {
+ text = resources.getString(R.string.recover)
+ setOnClickListener {
+ RecoverOrDeleteManager.showRecoveringDialog(
+ supportFragmentManager,
+ lifecycleScope,
+ setOf(resultPhotosFiles)
+ ) { count ->
+ complete(count, 0)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun startProgressUpdater() {
+ updateHandler.post(object : Runnable {
+ override fun run() {
+ if (player.isPlaying || player.isLoading) {
+ val pos = player.currentPosition
+ val dur = player.duration.takeIf { it > 0 } ?: 1L
+ val progress = (pos * 100 / dur).toInt()
+ binding.seekBar.progress = progress
+
+ binding.textTimeCurrent.text = Common.formatDuration(pos)
+ binding.textTimeTotal.text = Common.formatDuration(dur)
+ }
+ updateHandler.postDelayed(this, 500)
+ }
+ })
+ }
+
+ private fun initPlayer() {
+ myData?.let {
+ player = ExoPlayer.Builder(this).build()
+ binding.playerView.player = player
+ val mediaItem = MediaItem.fromUri(Uri.fromFile(it.targetFile))
+ player.addListener(object : Player.Listener {
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ super.onPlaybackStateChanged(playbackState)
+ when (playbackState) {
+ Player.STATE_IDLE -> {
+
+ }
+
+ Player.STATE_BUFFERING -> {
+
+ }
+
+ Player.STATE_READY -> {
+
+ }
+
+ Player.STATE_ENDED -> {
+ binding.playImage.isSelected = false
+ }
+ }
+
+ }
+ })
+ player.setMediaItem(mediaItem)
+ player.prepare()
+ }
+
+ }
+
+
+ private fun complete(number: Int, type: Int) {
+ finish()
+ startActivity(Intent(this@VideoPlayActivity, RecoverySuccessActivity::class.java).apply {
+ putExtra(RecoverySuccessActivity.KEY_SUCCESS_COUNT, number)
+ putExtra(RecoverySuccessActivity.KEY_SUCCESS_TYPE, type)
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/back_white.png b/app/src/main/res/drawable/back_white.png
new file mode 100644
index 0000000..9a7a0d9
Binary files /dev/null and b/app/src/main/res/drawable/back_white.png differ
diff --git a/app/src/main/res/drawable/icon_finished.png b/app/src/main/res/drawable/icon_finished.png
new file mode 100644
index 0000000..fc7b43e
Binary files /dev/null and b/app/src/main/res/drawable/icon_finished.png differ
diff --git a/app/src/main/res/drawable/icon_folder_audio.png b/app/src/main/res/drawable/icon_folder_audio.png
new file mode 100644
index 0000000..fd56bf2
Binary files /dev/null and b/app/src/main/res/drawable/icon_folder_audio.png differ
diff --git a/app/src/main/res/drawable/icon_folder_documents.png b/app/src/main/res/drawable/icon_folder_documents.png
new file mode 100644
index 0000000..23e1485
Binary files /dev/null and b/app/src/main/res/drawable/icon_folder_documents.png differ
diff --git a/app/src/main/res/drawable/icon_info_pause.png b/app/src/main/res/drawable/icon_info_pause.png
new file mode 100644
index 0000000..1a53755
Binary files /dev/null and b/app/src/main/res/drawable/icon_info_pause.png differ
diff --git a/app/src/main/res/drawable/icon_info_play.png b/app/src/main/res/drawable/icon_info_play.png
new file mode 100644
index 0000000..e293c44
Binary files /dev/null and b/app/src/main/res/drawable/icon_info_play.png differ
diff --git a/app/src/main/res/drawable/icon_item_audio_play.png b/app/src/main/res/drawable/icon_item_audio_play.png
new file mode 100644
index 0000000..baf4e3b
Binary files /dev/null and b/app/src/main/res/drawable/icon_item_audio_play.png differ
diff --git a/app/src/main/res/drawable/icon_small_audio.png b/app/src/main/res/drawable/icon_small_audio.png
new file mode 100644
index 0000000..3922693
Binary files /dev/null and b/app/src/main/res/drawable/icon_small_audio.png differ
diff --git a/app/src/main/res/drawable/icon_type_photo.png b/app/src/main/res/drawable/icon_type_photo.png
new file mode 100644
index 0000000..bad1d06
Binary files /dev/null and b/app/src/main/res/drawable/icon_type_photo.png differ
diff --git a/app/src/main/res/drawable/icon_type_video.png b/app/src/main/res/drawable/icon_type_video.png
new file mode 100644
index 0000000..a1f7ca0
Binary files /dev/null and b/app/src/main/res/drawable/icon_type_video.png differ
diff --git a/app/src/main/res/drawable/im_audio_center_image.png b/app/src/main/res/drawable/im_audio_center_image.png
new file mode 100644
index 0000000..5b6561c
Binary files /dev/null and b/app/src/main/res/drawable/im_audio_center_image.png differ
diff --git a/app/src/main/res/drawable/im_documents_center_image.png b/app/src/main/res/drawable/im_documents_center_image.png
new file mode 100644
index 0000000..1fa0708
Binary files /dev/null and b/app/src/main/res/drawable/im_documents_center_image.png differ
diff --git a/app/src/main/res/drawable/im_video_center_image.png b/app/src/main/res/drawable/im_video_center_image.png
new file mode 100644
index 0000000..08ef0fc
Binary files /dev/null and b/app/src/main/res/drawable/im_video_center_image.png differ
diff --git a/app/src/main/res/drawable/seekbar_thumb.xml b/app/src/main/res/drawable/seekbar_thumb.xml
new file mode 100644
index 0000000..5420ffb
--- /dev/null
+++ b/app/src/main/res/drawable/seekbar_thumb.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/seekbar_video_play.xml b/app/src/main/res/drawable/seekbar_video_play.xml
new file mode 100644
index 0000000..3f5a1f4
--- /dev/null
+++ b/app/src/main/res/drawable/seekbar_video_play.xml
@@ -0,0 +1,25 @@
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/selector_play_button.xml b/app/src/main/res/drawable/selector_play_button.xml
new file mode 100644
index 0000000..3e83ce9
--- /dev/null
+++ b/app/src/main/res/drawable/selector_play_button.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_photo_info.xml b/app/src/main/res/layout/activity_photo_info.xml
index 5a71be9..f94842a 100644
--- a/app/src/main/res/layout/activity_photo_info.xml
+++ b/app/src/main/res/layout/activity_photo_info.xml
@@ -48,13 +48,30 @@
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent">
-
+ android:id="@+id/frame_image"
+ android:layout_height="320dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_photo_sorting.xml b/app/src/main/res/layout/activity_photo_sorting.xml
index 8d81bf8..5e54de9 100644
--- a/app/src/main/res/layout/activity_photo_sorting.xml
+++ b/app/src/main/res/layout/activity_photo_sorting.xml
@@ -52,13 +52,13 @@
android:orientation="horizontal">
@@ -81,13 +81,13 @@
android:orientation="horizontal">
@@ -125,48 +125,55 @@
-
+ android:id="@+id/relative_thumbnails"
+ android:layout_below="@id/filter_date_layout">
-
+
+
+
+
+
+
-
diff --git a/app/src/main/res/layout/activity_scan_result_display.xml b/app/src/main/res/layout/activity_scan_result_display.xml
index 160f102..e4f71d0 100644
--- a/app/src/main/res/layout/activity_scan_result_display.xml
+++ b/app/src/main/res/layout/activity_scan_result_display.xml
@@ -8,6 +8,7 @@
android:background="@color/white"
android:orientation="vertical"
tools:context=".result.ScanResultDisplayActivity">
+
+ android:textSize="14sp"
+ app:fontType="bold" />
+ android:textSize="14sp"
+ app:fontType="bold" />
-
+ android:layout_height="match_parent"
+ android:background="@drawable/bg_rectangle_white_top_20"
+ android:layout_marginTop="20dp">
+
+
+
+
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_scanning.xml b/app/src/main/res/layout/activity_scanning.xml
index aa4459b..cee4ad8 100644
--- a/app/src/main/res/layout/activity_scanning.xml
+++ b/app/src/main/res/layout/activity_scanning.xml
@@ -34,67 +34,71 @@
app:fontType="bold" />
-
-
-
+ android:orientation="horizontal"
+ android:paddingTop="150dp">
+
+
+ app:fontType="bold"
+ app:layout_constraintEnd_toStartOf="@id/tv_scan_describe"
+ app:layout_constraintHorizontal_weight="1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/scan_progress" />
-
+ app:fontType="bold"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="1"
+ app:layout_constraintStart_toEndOf="@id/tv_scan_current_counts"
+ app:layout_constraintTop_toTopOf="@id/tv_scan_current_counts" />
-
-
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="103dp"
+ android:indeterminateTint="@color/main_title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_scan_current_counts" />
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_video_play.xml b/app/src/main/res/layout/activity_video_play.xml
new file mode 100644
index 0000000..478ab05
--- /dev/null
+++ b/app/src/main/res/layout/activity_video_play.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/file_span_count_three_adapter.xml b/app/src/main/res/layout/file_span_count_three_adapter.xml
index 908ef2f..af9fb5d 100644
--- a/app/src/main/res/layout/file_span_count_three_adapter.xml
+++ b/app/src/main/res/layout/file_span_count_three_adapter.xml
@@ -3,10 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
- android:layout_marginEnd="10dp"
- android:layout_marginTop="9dp"
android:layout_width="match_parent"
- android:layout_height="150dp">
+ android:layout_height="150dp"
+ android:layout_marginTop="9dp"
+ android:layout_marginEnd="10dp">
-
+ android:layout_alignParentBottom="true">
+
+
+
+
+
-
+ android:background="@drawable/photo_size_bg">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/scan_result_documents_adapter.xml b/app/src/main/res/layout/scan_result_documents_adapter.xml
new file mode 100644
index 0000000..1777d40
--- /dev/null
+++ b/app/src/main/res/layout/scan_result_documents_adapter.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index eb2472f..03ff289 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -24,6 +24,7 @@
#15787880
#F2F2F7
#D9D9D9
+ #99F2F2F7
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eb6b458..4bb8853 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -36,12 +36,16 @@
Exit
Allow
Scanning…
+ Photos
photos
deleted photos
+ Videos
videos
deleted videos
+ Audios
audios
deleted audios
+ Documents
documents
deleted documents
Finished!
@@ -49,6 +53,7 @@
If you exit,the scanning results will be discarded.Are you sure you want to exit now?
Date
Size
+ Duration
Layout
Hide thumbnails (%d)
Thumbnails refer to photos below 256 pixels
@@ -64,6 +69,7 @@
OK
Name
Path
+ Type
Resolution
Recovering...
It may take a few seconds to recover the file(s), please
@@ -78,6 +84,8 @@ wait..
The file(s) will be completely deleted and cannot be recovered.
Confirm delete?
View
+ Sorry!No %s found!
+
- All
@@ -86,12 +94,27 @@ wait..
- within 24 month
- Customize
-
+
- All
- 0-1 M
- 1-5 M
- >5 M
+
+
+ - All
+ - 0-500 KB
+ - 500 KB-1 M
+ - >1 M
+
+
+
+ - All
+ - 0-5 minutes
+ - 5-20 minutes
+ - 20-60 minutes
+ - >60 minutes
+
- 2 columns
- 3 columns