This commit is contained in:
kunfei
2023-04-01 01:23:13 +08:00
parent 14ee3f0a8e
commit d81685ab86
18 changed files with 325 additions and 376 deletions
@@ -1,23 +1,35 @@
package io.legado.app.ui.file
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogFileChooserBinding
import io.legado.app.databinding.ItemFilePickerBinding
import io.legado.app.databinding.ItemPathPickerBinding
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.getPrimaryDisabledTextColor
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.file.HandleFileContract.Companion.DIR
import io.legado.app.ui.file.HandleFileContract.Companion.FILE
import io.legado.app.ui.file.adapter.FileAdapter
import io.legado.app.ui.file.adapter.PathAdapter
import io.legado.app.ui.file.utils.FilePickerIcon
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -25,9 +37,7 @@ import java.io.File
class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
Toolbar.OnMenuItemClickListener,
FileAdapter.CallBack,
PathAdapter.CallBack {
Toolbar.OnMenuItemClickListener {
companion object {
const val tag = "FileChooserDialog"
@@ -37,40 +47,26 @@ class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
mode: Int = FILE,
title: String? = null,
initPath: String? = null,
isShowHomeDir: Boolean = false,
isShowUpDir: Boolean = true,
isShowHideDir: Boolean = false,
allowExtensions: Array<String>? = null,
menus: Array<String>? = null
) {
FilePickerDialog().apply {
val bundle = Bundle()
bundle.putInt("mode", mode)
bundle.putString("title", title)
bundle.putBoolean("isShowHomeDir", isShowHomeDir)
bundle.putBoolean("isShowUpDir", isShowUpDir)
bundle.putBoolean("isShowHideDir", isShowHideDir)
bundle.putString("initPath", initPath)
bundle.putStringArray("allowExtensions", allowExtensions)
bundle.putStringArray("menus", menus)
arguments = bundle
}.show(manager, tag)
}
}
private val binding by viewBinding(DialogFileChooserBinding::bind)
override var allowExtensions: Array<String>? = null
override val isSelectDir: Boolean
get() = mode == DIR
override var isShowHomeDir: Boolean = false
override var isShowUpDir: Boolean = true
override var isShowHideDir: Boolean = false
var title: String? = null
private var initPath = FileUtils.getSdCardPath()
private var mode: Int = FILE
private lateinit var fileAdapter: FileAdapter
private lateinit var pathAdapter: PathAdapter
private var menus: Array<String>? = null
private val viewModel by viewModels<FilePickerViewModel>()
private val dirParent = ".."
private val pathAdapter by lazy { PathAdapter() }
private val fileAdapter by lazy { FileAdapter() }
override fun onStart() {
super.onStart()
@@ -80,20 +76,8 @@ class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
view.setBackgroundResource(R.color.background_card)
arguments?.let {
mode = it.getInt("mode", FILE)
title = it.getString("title")
isShowHomeDir = it.getBoolean("isShowHomeDir")
isShowUpDir = it.getBoolean("isShowUpDir")
isShowHideDir = it.getBoolean("isShowHideDir")
it.getString("initPath")?.let { path ->
initPath = path
}
allowExtensions = it.getStringArray("allowExtensions")
menus = it.getStringArray("menus")
}
binding.toolBar.title = title ?: let {
if (isSelectDir) {
binding.toolBar.title = arguments?.getString("title") ?: let {
if (viewModel.isSelectDir) {
getString(R.string.folder_chooser)
} else {
getString(R.string.file_chooser)
@@ -101,93 +85,67 @@ class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
}
initMenu()
initContentView()
refreshCurrentDirPath(initPath)
viewModel.filesLiveData.observe(viewLifecycleOwner) {
fileAdapter.selectFile = null
fileAdapter.setItems(it)
}
viewModel.initData(arguments)
}
private fun initMenu() {
binding.toolBar.inflateMenu(R.menu.file_chooser)
if (isSelectDir) {
binding.toolBar.menu.findItem(R.id.menu_ok).isVisible = true
}
menus?.let {
it.forEach { menuTitle ->
binding.toolBar.menu.add(menuTitle)
}
}
binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener(this)
}
private fun initContentView() {
fileAdapter = FileAdapter(requireContext(), this)
pathAdapter = PathAdapter(requireContext(), this)
binding.rvPath.layoutManager = LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false)
binding.rvPath.adapter = pathAdapter
binding.rvFile.addItemDecoration(VerticalDivider(requireContext()))
binding.rvFile.layoutManager = LinearLayoutManager(activity)
binding.rvFile.adapter = fileAdapter
binding.rvPath.layoutManager = LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false)
binding.rvPath.adapter = pathAdapter
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_ok -> fileAdapter.currentPath?.let {
setData(it)
dismissAllowingStateLoss()
}
}
return true
}
override fun onFileClick(position: Int) {
val fileItem = fileAdapter.getItem(position)
if (fileItem?.isDirectory == true) {
refreshCurrentDirPath(fileItem.path)
} else {
fileItem?.path?.let { path ->
if (mode == DIR) {
toastOnUi("这是文件夹选择,不能选择文件,点击右上角的确定选择文件夹")
} else if (allowExtensions.isNullOrEmpty() ||
allowExtensions?.contains(FileUtils.getExtension(path)) == true
) {
setData(path)
binding.tvOk.setOnClickListener {
if (viewModel.isSelectDir) {
viewModel.lastDir?.let {
setResultData(it.path)
dismissAllowingStateLoss()
}
} else {
val file = fileAdapter.selectFile
if (file == null) {
toastOnUi("请选择文件")
} else {
toastOnUi("不能打开此文件")
setResultData(file.path)
dismissAllowingStateLoss()
}
}
}
}
override fun onPathClick(position: Int) {
refreshCurrentDirPath(pathAdapter.getPath(position))
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_create -> alert(R.string.create_folder) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "文件夹名"
}
customView { alertBinding.root }
okButton {
val text = alertBinding.editView.text?.toString()
if (text.isNullOrBlank()) {
toastOnUi("文件夹名不能为空")
} else {
viewModel.createFolder(text.trim())
}
}
cancelButton()
}
}
return true
}
private fun refreshCurrentDirPath(currentPath: String) {
if (currentPath == "/") {
pathAdapter.updatePath("/")
} else {
pathAdapter.updatePath(currentPath)
}
fileAdapter.loadData(currentPath)
var adapterCount = fileAdapter.itemCount
if (isShowHomeDir) {
adapterCount--
}
if (isShowUpDir) {
adapterCount--
}
if (adapterCount < 1) {
binding.tvEmpty.visible()
binding.tvEmpty.setText(R.string.empty)
} else {
binding.tvEmpty.gone()
}
}
private fun setData(path: String) {
private fun setResultData(path: String) {
val data = Intent().setData(Uri.fromFile(File(path)))
(parentFragment as? CallBack)?.onResult(data)
(activity as? CallBack)?.onResult(data)
@@ -198,6 +156,134 @@ class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
activity?.finish()
}
@SuppressLint("SetTextI18n")
inner class PathAdapter :
RecyclerAdapter<File, ItemPathPickerBinding>(requireContext()) {
private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getArrow())
init {
addHeaderView {
ItemPathPickerBinding.inflate(inflater, it, false).apply {
textView.text = "root"
imageView.setImageDrawable(arrowIcon)
root.setOnClickListener {
viewModel.subDocs.clear()
setItems(viewModel.subDocs)
viewModel.upFiles(viewModel.rootDoc)
}
}
}
}
override fun getViewBinding(parent: ViewGroup): ItemPathPickerBinding {
return ItemPathPickerBinding.inflate(inflater, parent, false).apply {
imageView.setImageDrawable(arrowIcon)
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemPathPickerBinding) {
binding.root.setOnClickListener {
viewModel.subDocs = viewModel.subDocs.subList(0, holder.layoutPosition)
setItems(viewModel.subDocs)
viewModel.upFiles(viewModel.subDocs.lastOrNull())
}
}
override fun convert(
holder: ItemViewHolder,
binding: ItemPathPickerBinding,
item: File,
payloads: MutableList<Any>
) {
binding.textView.text = item.name
}
}
inner class FileAdapter : RecyclerAdapter<File, ItemFilePickerBinding>(requireContext()) {
private val primaryTextColor = context.getPrimaryTextColor(!AppConfig.isNightTheme)
private val disabledTextColor = context.getPrimaryDisabledTextColor(!AppConfig.isNightTheme)
private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir())!!
private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder())!!
private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile())!!
private val selectDrawable =
ResourcesCompat.getDrawable(resources, R.drawable.shape_radius_1dp, null)!!.apply {
DrawableCompat.setTint(this, primaryTextColor)
}
var selectFile: File? = null
override fun getViewBinding(parent: ViewGroup): ItemFilePickerBinding {
return ItemFilePickerBinding.inflate(inflater, parent, false)
}
override fun registerListener(holder: ItemViewHolder, binding: ItemFilePickerBinding) {
binding.root.setOnClickListener {
val item = getItemByLayoutPosition(holder.layoutPosition)
item?.let {
if (item == viewModel.lastDir) {
viewModel.subDocs.removeLastOrNull()
pathAdapter.setItems(viewModel.subDocs)
viewModel.upFiles(viewModel.subDocs.lastOrNull() ?: viewModel.rootDoc)
} else if (item.isDirectory) {
viewModel.subDocs.add(item)
pathAdapter.setItems(viewModel.subDocs)
viewModel.upFiles(item)
} else if (viewModel.isSelectFile) {
viewModel.allowExtensions.let {
if (it.isNullOrEmpty() || it.contains(FileUtils.getExtension(item.path))) {
selectFile = item
notifyItemRangeChanged(getHeaderCount(), itemCount, "selectFile")
}
}
}
}
}
}
override fun convert(
holder: ItemViewHolder,
binding: ItemFilePickerBinding,
item: File,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
if (item == viewModel.lastDir) {
binding.imageView.setImageDrawable(upIcon)
binding.textView.text = dirParent
} else if (item.isDirectory) {
binding.imageView.setImageDrawable(folderIcon)
binding.textView.text = item.name
} else {
binding.imageView.setImageDrawable(fileIcon)
binding.textView.text = item.name
}
if (item.isDirectory) {
binding.textView.setTextColor(primaryTextColor)
} else {
if (viewModel.isSelectDir) {
binding.textView.setTextColor(disabledTextColor)
} else {
viewModel.allowExtensions?.let {
if (it.isEmpty() || it.contains(FileUtils.getExtension(item.path))) {
binding.textView.setTextColor(primaryTextColor)
} else {
binding.textView.setTextColor(disabledTextColor)
}
} ?: binding.textView.setTextColor(primaryTextColor)
}
}
}
binding.root.isSelected = item == selectFile
if (item == selectFile) {
binding.root.background = selectDrawable
} else {
binding.root.setBackgroundColor(getCompatColor(R.color.transparent))
}
}
}
interface CallBack {
fun onResult(data: Intent)
}
@@ -0,0 +1,76 @@
package io.legado.app.ui.file
import android.app.Application
import android.os.Bundle
import android.os.Environment
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.exception.NoStackTraceException
import io.legado.app.utils.toastOnUi
import java.io.File
class FilePickerViewModel(application: Application) : BaseViewModel(application) {
var rootDoc: File? = Environment.getExternalStorageDirectory()
var subDocs = mutableListOf<File>()
val filesLiveData = MutableLiveData<List<File>>()
var mode: Int = HandleFileContract.FILE
var isShowHideDir: Boolean = false
var allowExtensions: Array<String>? = null
val isSelectDir: Boolean get() = mode == HandleFileContract.DIR
val isSelectFile: Boolean get() = mode == HandleFileContract.FILE
val lastDir: File? get() = subDocs.lastOrNull() ?: rootDoc
fun initData(arguments: Bundle?) {
arguments?.let {
mode = it.getInt("mode", HandleFileContract.FILE)
isShowHideDir = it.getBoolean("isShowHideDir")
it.getString("initPath")?.let { path ->
rootDoc = File(path)
}
allowExtensions = it.getStringArray("allowExtensions")
}
upFiles(rootDoc)
}
fun upFiles(parentFile: File?) {
execute {
parentFile ?: return@execute emptyList()
if (parentFile == rootDoc) {
parentFile.listFiles()?.sortedWith(
compareBy({ it.isFile }, { it.name })
)
} else {
val list = arrayListOf(parentFile)
parentFile.listFiles()?.sortedWith(
compareBy({ it.isFile }, { it.name })
)?.let {
list.addAll(it)
}
list
}
}.onStart {
filesLiveData.postValue(emptyList())
}.onSuccess {
filesLiveData.postValue(it ?: emptyList())
}.onError {
context.toastOnUi(it.localizedMessage)
}
}
fun createFolder(name: String) {
execute {
val dir = lastDir ?: throw NoStackTraceException("父文件夹不存在")
val folder = File(dir, name)
if (!folder.canonicalPath.contains(dir.canonicalPath)) {
throw NoStackTraceException("非法文件名")
}
folder.mkdir()
}.onSuccess {
upFiles(lastDir)
}.onError {
context.toastOnUi(it.localizedMessage)
}
}
}
@@ -1,158 +0,0 @@
package io.legado.app.ui.file.adapter
import android.content.Context
import android.view.ViewGroup
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemFilePickerBinding
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.theme.getPrimaryDisabledTextColor
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.ui.file.entity.FileItem
import io.legado.app.ui.file.utils.FilePickerIcon
import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.FileUtils
import java.io.File
class FileAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<FileItem, ItemFilePickerBinding>(context) {
private var rootPath: String? = null
var currentPath: String? = null
private set
private val homeIcon = ConvertUtils.toDrawable(FilePickerIcon.getHome())!!
private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir())!!
private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder())!!
private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile())!!
private val primaryTextColor = context.getPrimaryTextColor(!AppConfig.isNightTheme)
private val disabledTextColor = context.getPrimaryDisabledTextColor(!AppConfig.isNightTheme)
private val dirRoot = "."
private val dirParent = ".."
fun loadData(path: String?) {
if (path == null) {
return
}
val data = ArrayList<FileItem>()
if (rootPath == null) {
rootPath = path
}
currentPath = path
if (callBack.isShowHomeDir) {
//添加“返回主目录”
val fileRoot = FileItem(
isDirectory = true,
icon = homeIcon,
name = dirRoot,
path = rootPath ?: path
)
data.add(fileRoot)
}
if (callBack.isShowUpDir && path != PathAdapter.sdCardDirectory) {
//添加“返回上一级目录”
val fileParent = FileItem(
isDirectory = true,
icon = upIcon,
name = dirParent,
path = File(path).parent ?: ""
)
data.add(fileParent)
}
currentPath?.let { currentPath ->
val files: Array<File>? = FileUtils.listDirsAndFiles(currentPath)
if (files != null) {
for (file in files) {
if (!callBack.isShowHideDir && file.name.startsWith(".")) {
continue
}
val fileItem = if (file.isDirectory) {
FileItem(
name = file.name,
icon = folderIcon,
path = file.absolutePath,
isDirectory = true
)
} else {
FileItem(
name = file.name,
icon = fileIcon,
path = file.absolutePath,
size = file.length(),
isDirectory = true
)
}
data.add(fileItem)
}
}
setItems(data)
}
}
override fun getViewBinding(parent: ViewGroup): ItemFilePickerBinding {
return ItemFilePickerBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemFilePickerBinding,
item: FileItem,
payloads: MutableList<Any>
) {
binding.apply {
imageView.setImageDrawable(item.icon)
textView.text = item.name
if (item.isDirectory) {
textView.setTextColor(primaryTextColor)
} else {
if (callBack.isSelectDir) {
textView.setTextColor(disabledTextColor)
} else {
callBack.allowExtensions?.let {
if (it.isEmpty() || it.contains(FileUtils.getExtension(item.path))) {
textView.setTextColor(primaryTextColor)
} else {
textView.setTextColor(disabledTextColor)
}
} ?: textView.setTextColor(primaryTextColor)
}
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemFilePickerBinding) {
holder.itemView.setOnClickListener {
callBack.onFileClick(holder.layoutPosition)
}
}
interface CallBack {
fun onFileClick(position: Int)
//允许的扩展名
var allowExtensions: Array<String>?
/**
* 是否选取目录
*/
val isSelectDir: Boolean
/**
* 是否显示返回主目录
*/
var isShowHomeDir: Boolean
/**
* 是否显示返回上一级
*/
var isShowUpDir: Boolean
/**
* 是否显示隐藏的目录(以“.”开头)
*/
var isShowHideDir: Boolean
}
}
@@ -1,78 +0,0 @@
package io.legado.app.ui.file.adapter
import android.content.Context
import android.os.Environment
import android.view.ViewGroup
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemPathPickerBinding
import io.legado.app.ui.file.utils.FilePickerIcon
import io.legado.app.utils.ConvertUtils
import java.util.*
class PathAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<String, ItemPathPickerBinding>(context) {
private val paths = LinkedList<String>()
private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getArrow())
fun getPath(position: Int): String {
val tmp = StringBuilder("$sdCardDirectory/")
//忽略根目录
if (position == 0) {
return tmp.toString()
}
for (i in 1..position) {
tmp.append(paths[i]).append("/")
}
return tmp.toString()
}
fun updatePath(path: String) {
var path1 = path
path1 = path1.replace(sdCardDirectory, "")
paths.clear()
if (path1 != "/" && path1 != "") {
val subDirs = path1.substring(path1.indexOf("/") + 1)
.split("/")
.dropLastWhile { it.isEmpty() }
.toTypedArray()
Collections.addAll(paths, *subDirs)
}
paths.addFirst(ROOT_HINT)
setItems(paths)
}
override fun getViewBinding(parent: ViewGroup): ItemPathPickerBinding {
return ItemPathPickerBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemPathPickerBinding,
item: String,
payloads: MutableList<Any>
) {
binding.apply {
textView.text = item
imageView.setImageDrawable(arrowIcon)
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemPathPickerBinding) {
holder.itemView.setOnClickListener {
callBack.onPathClick(holder.layoutPosition)
}
}
interface CallBack {
fun onPathClick(position: Int)
}
companion object {
private const val ROOT_HINT = "SD"
@Suppress("DEPRECATION")
val sdCardDirectory: String = Environment.getExternalStorageDirectory().absolutePath
}
}
@@ -1,14 +0,0 @@
package io.legado.app.ui.file.entity
import android.graphics.drawable.Drawable
/**
* 文件项信息
*/
data class FileItem(
var icon: Drawable,
var name: String,
var path: String = "/",
var size: Long = 0,
var isDirectory: Boolean = false,
)