This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

@@ -0,0 +1,231 @@
package io.legato.kazusa.ui.file
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.databinding.ActivityFileManageBinding
import io.legato.kazusa.databinding.ItemFileBinding
import io.legato.kazusa.databinding.ItemPathPickerBinding
//import io.legado.app.lib.theme.primaryTextColor
import io.legato.kazusa.ui.file.utils.FilePickerIcon
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.ConvertUtils
import io.legato.kazusa.utils.applyNavigationBarPadding
import io.legato.kazusa.utils.openFileUri
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import java.io.File
class FileManageActivity : VMBaseActivity<ActivityFileManageBinding, FileManageViewModel>() {
override val binding by viewBinding(ActivityFileManageBinding::inflate)
override val viewModel by viewModels<FileManageViewModel>()
private val dirParent = ".."
private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view)
}
private val pathAdapter by lazy {
PathAdapter()
}
private val fileAdapter by lazy {
FileAdapter()
}
private val currentFiles = arrayListOf<File>()
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
initSearchView()
viewModel.upFiles(viewModel.rootDoc)
}
private fun initView() {
binding.rvPath.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
binding.rvPath.adapter = pathAdapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.addItemDecoration(VerticalDivider(this))
binding.recyclerView.adapter = fileAdapter
binding.recyclerView.applyNavigationBarPadding()
onBackPressedDispatcher.addCallback(this) {
if (viewModel.lastDir != viewModel.rootDoc) {
gotoLastDir()
return@addCallback
}
finish()
}
}
private fun initSearchView() {
//searchView.applyTint(primaryTextColor)
searchView.queryHint = getString(R.string.screen) + " • " + getString(R.string.file_manage)
searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateFiles()
return false
}
})
}
private fun updateFiles() {
if (searchView.query.isNotEmpty()) {
currentFiles.filter {
it.name == dirParent || it.name.contains(searchView.query)
}.let {
fileAdapter.setItems(it)
}
} else {
fileAdapter.setItems(currentFiles)
}
}
private fun gotoLastDir() {
viewModel.subDocs.removeLastOrNull()
pathAdapter.setItems(viewModel.subDocs)
viewModel.upFiles(viewModel.lastDir)
}
override fun observeLiveBus() {
viewModel.filesLiveData.observe(this) {
searchView.setQuery("", false)
currentFiles.clear()
currentFiles.addAll(it)
updateFiles()
}
}
@SuppressLint("SetTextI18n")
inner class PathAdapter :
RecyclerAdapter<File, ItemPathPickerBinding>(this@FileManageActivity) {
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, ItemFileBinding>(this@FileManageActivity) {
private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir())!!
private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder())!!
private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile())!!
override fun getViewBinding(parent: ViewGroup): ItemFileBinding {
return ItemFileBinding.inflate(inflater, parent, false)
}
override fun registerListener(holder: ItemViewHolder, binding: ItemFileBinding) {
binding.root.setOnClickListener {
val item = getItemByLayoutPosition(holder.layoutPosition)
item?.let {
if (item == viewModel.lastDir) {
gotoLastDir()
} else if (item.isDirectory) {
viewModel.subDocs.add(item)
pathAdapter.setItems(viewModel.subDocs)
viewModel.upFiles(item)
} else {
openFileUri(
FileProvider.getUriForFile(
this@FileManageActivity,
AppConst.authority,
item
)
)
}
}
}
binding.root.setOnLongClickListener { view ->
val item = getItemByLayoutPosition(holder.layoutPosition)
if (item == viewModel.lastDir) {
return@setOnLongClickListener true
}
item?.let {
showFileMenu(view, item)
}
return@setOnLongClickListener true
}
}
override fun convert(
holder: ItemViewHolder,
binding: ItemFileBinding,
item: File,
payloads: MutableList<Any>
) {
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
}
}
private fun showFileMenu(view: View, file: File) {
val popupMenu = PopupMenu(context, view)
popupMenu.inflate(R.menu.file_long_click)
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_del -> viewModel.delFile(file)
}
true
}
popupMenu.show()
}
}
}
@@ -0,0 +1,52 @@
package io.legato.kazusa.ui.file
import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.utils.toastOnUi
import java.io.File
class FileManageViewModel(application: Application) : BaseViewModel(application) {
val rootDoc = context.getExternalFilesDir(null)?.parentFile
var subDocs = mutableListOf<File>()
val filesLiveData = MutableLiveData<List<File>>()
val lastDir: File? get() = subDocs.lastOrNull() ?: 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 delFile(file: File) {
execute {
file.delete()
}.onSuccess {
upFiles(lastDir)
}.onError {
context.toastOnUi(it.localizedMessage)
}
}
}
@@ -0,0 +1,296 @@
package io.legato.kazusa.ui.file
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
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.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.databinding.DialogFileChooserBinding
import io.legato.kazusa.databinding.ItemFilePickerBinding
import io.legato.kazusa.databinding.ItemPathPickerBinding
import io.legato.kazusa.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.legato.kazusa.ui.file.HandleFileContract.Companion.FILE
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.FileUtils
import io.legato.kazusa.utils.applyTint
import io.legato.kazusa.utils.getCompatColor
import io.legato.kazusa.utils.setLayout
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import java.io.File
class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser),
Toolbar.OnMenuItemClickListener {
companion object {
const val tag = "FileChooserDialog"
fun show(
manager: FragmentManager,
mode: Int = FILE,
title: String? = null,
initPath: String? = null,
isShowHideDir: Boolean = false,
allowExtensions: Array<String>? = null,
) {
FilePickerDialog().apply {
val bundle = Bundle()
bundle.putInt("mode", mode)
bundle.putString("title", title)
bundle.putBoolean("isShowHideDir", isShowHideDir)
bundle.putString("initPath", initPath)
bundle.putStringArray("allowExtensions", allowExtensions)
arguments = bundle
}.show(manager, tag)
}
}
private val binding by viewBinding(DialogFileChooserBinding::bind)
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()
setLayout(0.9f, 0.8f)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
//view.setBackgroundResource(R.color.background_card)
initMenu()
initContentView()
viewModel.filesLiveData.observe(viewLifecycleOwner) {
fileAdapter.selectFile = null
fileAdapter.setItems(it)
}
viewModel.initData(arguments)
binding.toolBar.title = arguments?.getString("title") ?: let {
if (viewModel.isSelectDir) {
getString(R.string.folder_chooser)
} else {
getString(R.string.file_chooser)
}
}
}
private fun initMenu() {
binding.toolBar.inflateMenu(R.menu.file_chooser)
binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener(this)
}
private fun initContentView() {
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.tvOk.setOnClickListener {
if (viewModel.isSelectDir) {
viewModel.lastDir?.let {
setResultData(it.path)
dismissAllowingStateLoss()
}
} else {
val file = fileAdapter.selectFile
if (file == null) {
toastOnUi("请选择文件")
} else {
setResultData(file.path)
dismissAllowingStateLoss()
}
}
}
}
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 setResultData(path: String) {
val data = Intent().setData(Uri.fromFile(File(path)))
(parentFragment as? CallBack)?.onResult(data)
(activity as? CallBack)?.onResult(data)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
activity?.finish()
}
@SuppressLint("SetTextI18n")
inner class PathAdapter :
RecyclerAdapter<File, ItemPathPickerBinding>(requireContext()) {
private val arrowIcon = ContextCompat.getDrawable(requireContext(),R.drawable.ic_arrow_right)
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 upIcon = ContextCompat.getDrawable(requireContext(),R.drawable.ic_return)
private val folderIcon = ContextCompat.getDrawable(requireContext(),R.drawable.ic_folder)
private val fileIcon = ContextCompat.getDrawable(requireContext(),R.drawable.ic_file)
private val selectDrawable =
ResourcesCompat.getDrawable(resources, R.drawable.shape_radius_1dp, null)!!.apply {
// 从主题获取颜色
val typedValue = TypedValue()
requireContext().theme.resolveAttribute(android.R.attr.colorPrimary, typedValue, true)
val primaryColor = typedValue.data
DrawableCompat.setTint(this, primaryColor)
}
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.legato.kazusa.ui.file
import android.app.Application
import android.os.Bundle
import android.os.Environment
import androidx.lifecycle.MutableLiveData
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.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)
}
}
}
@@ -0,0 +1,309 @@
package io.legato.kazusa.ui.file
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.databinding.ActivityTranslucenceBinding
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.help.IntentData
import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.permission.Permissions
import io.legato.kazusa.lib.permission.PermissionsCompat
import io.legato.kazusa.utils.checkWrite
import io.legato.kazusa.utils.externalFiles
import io.legato.kazusa.utils.getJsonArray
import io.legato.kazusa.utils.isContentScheme
import io.legato.kazusa.utils.launch
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import splitties.init.appCtx
import java.io.File
class HandleFileActivity :
VMBaseActivity<ActivityTranslucenceBinding, HandleFileViewModel>(),
FilePickerDialog.CallBack {
override val binding by viewBinding(ActivityTranslucenceBinding::inflate)
override val viewModel by viewModels<HandleFileViewModel>()
private var mode = 0
private val selectDocTree =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
if (uri.isContentScheme()) {
val modeFlags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, modeFlags)
}
onResult(Intent().setData(uri))
} ?: finish()
}
private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
it?.let {
if (it.isContentScheme()) {
val modeFlags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(it, modeFlags)
}
onResult(Intent().setData(it))
} ?: finish()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
mode = intent.getIntExtra("mode", 0)
viewModel.errorLiveData.observe(this) {
toastOnUi(it)
finish()
}
val allowExtensions = intent.getStringArrayExtra("allowExtensions")
val selectList: ArrayList<SelectItem<Int>> = when (mode) {
HandleFileContract.DIR_SYS -> getDirActions(true)
HandleFileContract.DIR -> getDirActions()
HandleFileContract.FILE -> getFileActions()
HandleFileContract.EXPORT -> arrayListOf(
SelectItem(getString(R.string.upload_url), 111)
).apply {
addAll(getDirActions())
}
else -> arrayListOf()
}
intent.getJsonArray<SelectItem<Int>>("otherActions")?.let {
selectList.addAll(it)
}
val title = intent.getStringExtra("title") ?: let {
when (mode) {
HandleFileContract.EXPORT -> return@let getString(R.string.export)
HandleFileContract.DIR -> return@let getString(R.string.select_folder)
else -> return@let getString(R.string.select_file)
}
}
alert(title) {
items(selectList) { _, item, _ ->
when (item.value) {
HandleFileContract.DIR -> kotlin.runCatching {
selectDocTree.launch()
}.onFailure {
AppLog.put(getString(R.string.open_sys_dir_picker_error), it, true)
checkPermissions {
FilePickerDialog.show(
supportFragmentManager,
mode = HandleFileContract.DIR
)
}
}
HandleFileContract.FILE -> kotlin.runCatching {
selectDoc.launch(typesOfExtensions(allowExtensions))
}.onFailure {
AppLog.put(getString(R.string.open_sys_dir_picker_error), it, true)
checkPermissions {
FilePickerDialog.show(
supportFragmentManager,
mode = HandleFileContract.FILE,
allowExtensions = allowExtensions
)
}
}
10 -> checkPermissions {
@Suppress("DEPRECATION")
lifecycleScope.launchWhenResumed {
FilePickerDialog.show(
supportFragmentManager,
mode = HandleFileContract.DIR
)
}
}
11 -> checkPermissions {
@Suppress("DEPRECATION")
lifecycleScope.launchWhenResumed {
FilePickerDialog.show(
supportFragmentManager,
mode = HandleFileContract.FILE,
allowExtensions = allowExtensions
)
}
}
111 -> getFileData()?.let {
viewModel.upload(it.first, it.second, it.third) { url ->
val uri = Uri.parse(url)
setResult(RESULT_OK, Intent().setData(uri))
finish()
}
}
112 -> checkPermissions { // 手动输入目录路径
showInputDirectoryDialog()
}
else -> {
val path = item.title
val uri = if (path.isContentScheme()) {
Uri.parse(path)
} else {
Uri.fromFile(File(path))
}
onResult(Intent().setData(uri))
}
}
}
onCancelled {
finish()
}
}
}
private fun showInputDirectoryDialog() {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = getString(R.string.enter_directory_path)
}
alert(getString(R.string.manual_input)) {
customView { alertBinding.root }
okButton {
val inputPath = alertBinding.editView.text.toString()
if (inputPath.isBlank()) {
toastOnUi(getString(R.string.empty_directory_input))
return@okButton
}
val file = File(inputPath)
if (file.exists() &&
file.isDirectory &&
isExternalStorage(file) &&
file.checkWrite()
) {
onResult(Intent().setData(Uri.fromFile(file)))
} else {
toastOnUi(getString(R.string.invalid_directory))
}
}
onDismiss {
finish()
}
cancelButton()
}
}
private fun isExternalStorage(path: File): Boolean {
if (path.canonicalPath.startsWith(appCtx.externalFiles.parent!!)) {
return false
}
try {
if (Environment.isExternalStorageEmulated(path)) {
return true
}
} catch (_: IllegalArgumentException) {
}
try {
if (Environment.isExternalStorageRemovable(path)) {
return true
}
} catch (_: IllegalArgumentException) {
}
return false
}
private fun getFileData(): Triple<String, Any, String>? {
val fileName = intent.getStringExtra("fileName")
val file = intent.getStringExtra("fileKey")?.let {
IntentData.get<Any>(it)
}
val contentType = intent.getStringExtra("contentType")
if (fileName != null && file != null && contentType != null) {
return Triple(fileName, file, contentType)
}
return null
}
private fun getDirActions(onlySys: Boolean = false): ArrayList<SelectItem<Int>> {
return if (onlySys) {
arrayListOf(
SelectItem(getString(R.string.sys_folder_picker), HandleFileContract.DIR),
SelectItem(getString(R.string.manual_input), 112) // 添加手动输入选项
)
} else {
arrayListOf(
SelectItem(getString(R.string.sys_folder_picker), HandleFileContract.DIR),
SelectItem(getString(R.string.app_folder_picker), 10),
SelectItem(getString(R.string.manual_input), 112) // 添加手动输入选项
)
}
}
private fun getFileActions(): ArrayList<SelectItem<Int>> {
return arrayListOf(
SelectItem(getString(R.string.sys_file_picker), HandleFileContract.FILE),
SelectItem(getString(R.string.app_file_picker), 11)
)
}
private fun checkPermissions(success: (() -> Unit)? = null) {
PermissionsCompat.Builder()
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke()
}
.onDenied {
finish()
}
.onError {
finish()
}
.request()
}
private fun typesOfExtensions(allowExtensions: Array<String>?): Array<String> {
val types = hashSetOf<String>()
if (allowExtensions.isNullOrEmpty()) {
types.add("*/*")
} else {
allowExtensions.forEach {
when (it) {
"*" -> types.add("*/*")
"txt", "xml" -> types.add("text/*")
else -> {
val mime = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(it)
?: "application/octet-stream"
types.add(mime)
}
}
}
}
return types.toTypedArray()
}
override fun onResult(data: Intent) {
val uri = data.data
uri ?: let {
finish()
return
}
if (mode == HandleFileContract.EXPORT) {
getFileData()?.let { fileData ->
viewModel.saveToLocal(uri, fileData.first, fileData.second) { savedUri ->
setResult(RESULT_OK, Intent().setData(savedUri))
finish()
}
}
} else {
data.putExtra("value", intent.getStringExtra("value"))
setResult(RESULT_OK, data)
finish()
}
}
}
@@ -0,0 +1,85 @@
package io.legato.kazusa.ui.file
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import io.legato.kazusa.help.IntentData
import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.utils.RealPathUtil
import io.legato.kazusa.utils.externalFiles
import io.legato.kazusa.utils.putJson
import splitties.init.appCtx
@Suppress("unused")
class HandleFileContract :
ActivityResultContract<(HandleFileContract.HandleFileParam.() -> Unit)?, HandleFileContract.Result>() {
private var requestCode: Int = 0
override fun createIntent(context: Context, input: (HandleFileParam.() -> Unit)?): Intent {
val intent = Intent(context, HandleFileActivity::class.java)
val handleFileParam = HandleFileParam()
input?.let {
handleFileParam.apply(input)
}
handleFileParam.let {
requestCode = it.requestCode
intent.putExtra("mode", it.mode)
intent.putExtra("title", it.title)
intent.putExtra("allowExtensions", it.allowExtensions)
intent.putJson("otherActions", it.otherActions)
it.fileData?.let { fileData ->
intent.putExtra("fileName", fileData.name)
intent.putExtra("fileKey", IntentData.put(fileData.data))
intent.putExtra("contentType", fileData.type)
}
intent.putExtra("value", it.value)
}
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Result {
val uri = if (resultCode != RESULT_OK || intent?.data == null ||
RealPathUtil.getTreePath(intent.data!!)
?.startsWith(appCtx.externalFiles.parent!!) == true
) {
null
} else {
intent.data
}
return Result(uri, requestCode, intent?.getStringExtra("value"))
}
companion object {
const val DIR = 0
const val FILE = 1
const val DIR_SYS = 2
const val EXPORT = 3
}
@Suppress("ArrayInDataClass")
data class HandleFileParam(
var mode: Int = DIR,
var title: String? = null,
var allowExtensions: Array<String> = arrayOf(),
var otherActions: ArrayList<SelectItem<Int>>? = null,
var fileData: FileData? = null,
var requestCode: Int = 0,
var value: String? = null
)
data class Result(
val uri: Uri?,
val requestCode: Int,
val value: String?
)
data class FileData(
val name: String,
val data: Any,
val type: String
)
}
@@ -0,0 +1,63 @@
package io.legato.kazusa.ui.file
import android.app.Application
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.utils.*
import java.io.File
class HandleFileViewModel(application: Application) : BaseViewModel(application) {
val errorLiveData = MutableLiveData<String>()
fun upload(
fileName: String,
file: Any,
contentType: String,
success: (url: String) -> Unit
) {
execute {
DirectLinkUpload.upLoad(fileName, file, contentType)
}.onSuccess {
success.invoke(it)
}.onError {
AppLog.put("上传文件失败\n${it.localizedMessage}", it)
it.printOnDebug()
errorLiveData.postValue(it.localizedMessage)
}
}
fun saveToLocal(uri: Uri, fileName: String, data: Any, success: (uri: Uri) -> Unit) {
execute {
val bytes = when (data) {
is File -> data.readBytes()
is ByteArray -> data
is String -> data.toByteArray()
else -> GSON.toJson(data).toByteArray()
}
return@execute if (uri.isContentScheme()) {
val doc = DocumentFile.fromTreeUri(context, uri)!!
doc.findFile(fileName)?.delete()
val newDoc = doc.createFile("", fileName)
newDoc!!.writeBytes(context, bytes)
newDoc.uri
} else {
val file = File(uri.path ?: uri.toString())
val newFile = FileUtils.createFileIfNotExist(file, fileName)
newFile.writeBytes(bytes)
Uri.fromFile(newFile)
}
}.onError {
it.printOnDebug()
errorLiveData.postValue(it.localizedMessage)
}.onSuccess {
success.invoke(it)
}
}
}
@@ -0,0 +1,283 @@
package io.legato.kazusa.ui.file.utils;
/**
* Generated by https://github.com/gzu-liyujiang/Image2ByteVar
*
* @author 李玉江[QQ:1023694760]
* @since 2017/01/04 06:03
*/
public class FilePickerIcon {
public static byte[] getFile() {
return FILE;
}
public static byte[] getFolder() {
return FOLDER;
}
public static byte[] getHome() {
return HOME;
}
public static byte[] getUpDir() {
return UPDIR;
}
public static byte[] getArrow() {
return ARROW;
}
// fixed: 17-1-7 "static final" arrays should be "private"
private static final byte[] FILE = {
-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,
0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,
68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,
0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 1, -73, 73, 68, 65, 84,
88, -123, -19, -106, 63, 75, 3, 49, 24, -121, 127, -74, 69, -37, 110, -30, -94, -109, -120, -126,
-125, -101, 56, 72, 63, -125, -97, -64, -63, 77, 28, 69, 20, 81, 68, 113, 83, 113, -12, 91,
-120, -96, -117, -120, -85, 31, 65, 29, 28, -100, -124, -94, -101, 46, -83, -105, 92, 114, 113, -24,
31, -38, -110, 59, -34, -9, 114, 87, 69, -14, 64, 41, -92, 73, -34, -89, -55, -17, 114, 1,
60, 30, -113, -25, 79, 50, 66, -19, 120, -7, 16, 26, 0, -112, 82, -61, 24, 3, 41, 35,
0, -128, 20, 10, 74, 69, -48, 58, -126, 82, -90, -37, -90, -75, -127, 82, 81, 119, 124, 16,
40, -100, 109, 79, -109, -21, 13, 82, -94, 118, -108, 82, 99, -91, 54, -58, 25, 2, 0, 120,
-85, 7, -72, -71, 127, -57, -18, -58, 12, -102, -51, 87, 115, 113, 56, -105, 74, -74, 64, -19,
104, -116, 73, 51, 63, 0, -96, 88, 104, -107, 57, -34, -102, -59, -6, -63, 75, -86, -119, -56,
-94, -99, -83, -26, -96, 123, -122, 20, -37, -107, 78, -9, -25, -79, -74, -13, -52, -106, 37, -117,
114, -23, 72, 42, 109, -96, -93, 8, 66, 0, -97, 95, -83, -49, -55, -34, 2, 86, 55, 31,
89, -78, -12, -116, 10, -59, 18, -20, 48, 90, -86, -96, -47, -48, -72, -66, -5, 64, -40, -98,
-94, 90, -27, -81, 15, 89, -76, -9, 9, -114, 99, 80, 18, 0, -90, 38, -127, -38, -46, 4,
-76, 78, -97, 113, -128, 33, 42, -124, 78, -4, -35, 38, -87, -62, -42, -9, -14, -30, 120, 127,
-69, 6, -82, 110, -21, -44, -46, 0, -72, 103, 77, 12, 73, -110, 125, 109, -55, -1, 53, 17,
86, 70, 109, 66, 113, -72, 72, -39, 32, -89, -38, 53, 99, -82, 100, -14, 48, -39, -74, 57,
107, -100, -49, -47, -84, -77, 24, 7, 121, 69, -125, -64, 126, -114, 82, -91, -66, 3, 106, 37,
59, -12, 21, 85, -46, -87, 80, -91, -20, 52, -100, 46, -38, -108, -87, 111, 104, -103, -64, 58,
71, -121, -15, -48, -60, -63, -54, -24, -80, -14, 104, 35, -73, -37, 83, -42, -112, 69, 5, -15,
-10, -108, 23, -12, 3, 63, -92, -65, 63, 43, 101, -5, -10, 7, 14, -111, 112, -66, -108, -28,
-111, 71, 27, -1, 47, -93, -65, 77, 38, -9, 81, 27, 46, 121, -76, -63, 18, 29, 86, 30,
109, -80, 68, -113, -50, -97, -14, -14, -16, 120, 60, -98, 1, 126, 0, -110, 81, -78, -5, 36,
19, -64, 3, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126};
private static final byte[] FOLDER = {
-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,
0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,
68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,
0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -102, 73, 68, 65, 84,
88, -123, -19, -105, 61, -113, 19, 49, 16, -122, 95, 111, -110, 75, -124, -124, -60, -49, 65, 20,
-48, 34, 81, 80, 32, 81, 83, 80, -94, 52, 72, 8, 78, 66, -94, 64, 20, -108, 124, 20,
39, -47, 64, -51, -11, -7, 87, -96, 11, -38, -75, 61, 99, 15, -59, 110, 54, 118, -42, -5,
113, -71, 77, 117, -5, 54, -55, -50, -38, -42, -77, -81, 61, 99, 27, -104, 52, 105, -46, -92,
73, -73, 66, 106, 104, -61, -51, 57, -92, -21, -3, -29, 79, -61, -57, 58, 70, -13, 33, -115,
54, -25, -112, 71, -17, 127, 0, 0, -106, -38, -105, 65, 93, 64, -56, 0, 108, 33, 76, -72,
-28, -113, -14, -20, -77, 59, 25, 108, 54, 4, -14, -63, -6, 59, -112, 123, -84, -4, -84, -114,
-117, 39, 96, -74, -17, -2, -12, -59, 91, 92, -66, -103, 117, -70, 126, 19, 117, 58, -80, 57,
-121, 60, 92, 127, 5, 0, -84, -106, -53, -3, 11, 93, -108, -96, 0, 96, 52, -124, 9, 96,
6, 116, -127, -33, -65, -66, -44, -51, 56, -117, -121, -73, 62, 3, -7, 56, -74, 123, 126, -11,
-83, -24, 100, -23, -99, -6, -43, -67, -5, -119, -96, -119, -66, 80, 1, -128, 49, 0, -128, -25,
31, -98, -108, 65, 103, -93, 46, -62, -36, 24, -58, -27, -37, -6, -65, -39, -66, -108, -41, 63,
-13, 86, -40, 65, 107, 84, -7, 43, 64, -103, 70, 92, 108, 17, -45, 82, -94, -115, -47, 77,
-40, -22, 35, -108, 53, 32, 34, 56, 34, 112, -49, -14, 30, 4, 90, 66, -2, -119, 99, -34,
2, -69, -23, -33, -119, -10, -32, 18, -66, -37, -63, 114, 16, 99, -122, 2, -64, 87, 127, 97,
-56, -61, -53, 24, -96, -121, -14, -107, 35, 103, 11, -120, -91, 116, 27, -25, -45, -15, 96, 9,
80, -95, 97, -56, -61, 114, 127, 14, -10, 102, 125, 27, 36, -128, 86, -56, -70, 34, -52, 50,
-128, 109, -78, 77, 40, 118, -82, -73, 77, -65, -93, 98, 26, -128, -111, -62, 41, 62, -101, 67,
116, -111, 110, -41, 34, -53, 2, -26, 22, -9, 3, 29, 53, -11, -111, -109, -39, -94, -4, -83,
0, 85, -74, -120, -41, -25, 72, -22, 4, -27, -86, -58, -119, 45, -102, -119, 3, 52, -36, -124,
61, 40, 65, 81, -58, 55, -5, -117, 99, -80, 115, -89, 115, 52, 9, 93, 65, -90, -36, 76,
65, 94, 87, -67, -96, 74, -52, -2, 52, -46, 54, -91, -95, -109, -119, 108, 87, -13, -59, 126,
-9, 74, 40, -109, 27, 58, 106, 124, 6, -79, 40, -117, -7, 33, 100, 0, 23, -71, -72, -37,
-1, -85, 105, 31, -61, 77, 96, -24, -44, -109, 1, 40, -19, 70, 50, 113, -126, -75, -39, -22,
-26, 53, -85, -61, 113, 89, 127, 8, 23, 78, 119, 80, 55, -5, -36, -12, 117, 34, -11, 23,
-4, 78, 80, -14, 10, 112, 22, -30, 92, -5, -6, 28, 2, 25, -70, -103, 112, -46, -75, -19,
98, 67, 65, 57, 60, -110, -11, 13, 118, 4, -92, -9, 2, 79, 4, -94, 17, 118, 38, 97,
6, -104, 64, -108, -34, -103, -124, -35, -63, 115, -20, -68, -46, 58, 124, 2, 0, 56, 91, -18,
118, -123, -74, -48, 122, 88, -78, 117, -126, 90, 95, 102, -80, 49, 5, -88, 26, 80, -101, -46,
33, 83, -24, -42, 126, 53, 116, 42, 97, -104, -22, 2, -97, 111, 115, 108, -13, 17, 64, 1,
-64, 85, 103, 71, 50, 84, 1, -106, 110, 88, 106, 95, 10, 93, -5, -67, -81, 62, -44, 22,
26, 84, 1, -33, -67, -77, -24, 5, -19, -67, -116, 93, -84, 87, 2, 32, -70, 66, -104, -32,
-112, -53, 94, -63, -113, 112, 1, 125, 119, -15, -17, -92, -73, -40, 73, -109, 38, 77, -70, -19,
-6, 15, -2, -54, -98, -96, -19, -118, -95, -10, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66,
96, -126};
private static final byte[] HOME = {
-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,
0, 0, 0, 40, 8, 4, 0, 0, 0, 34, 2, -96, -37, 0, 0, 0, 1, 115, 82, 71,
66, 0, -82, -50, 28, -23, 0, 0, 0, 2, 98, 75, 71, 68, 0, -1, -121, -113, -52, -65,
0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1, 120, -91, 63,
118, 0, 0, 0, 7, 116, 73, 77, 69, 7, -37, 8, 4, 10, 36, 16, -22, -9, -18, -50,
0, 0, 2, -84, 73, 68, 65, 84, 72, -57, -19, -106, 91, 72, 20, 97, 20, -57, 127, -77,
-18, -106, -41, 54, -14, -106, -23, -102, 4, 21, 97, 5, 5, 61, 70, 74, -76, -122, -94, -11,
18, -108, -92, 121, 41, 16, -124, 96, -95, 48, -118, -108, -94, 18, -70, 96, 33, 68, -81, -127,
15, 25, 68, 15, 21, 89, 80, 42, 25, 42, -91, 97, -106, -103, -122, 107, 41, -19, -86, -37,
-22, 122, -39, 77, -73, -103, -81, 7, -105, 84, -36, -85, -26, -125, -44, -127, -31, 59, -52, -103,
-7, 113, -50, -103, -17, -4, -65, -127, -1, -74, 44, 76, -102, 113, -29, 22, -119, 50, -3, -15,
84, 75, -111, -87, -38, -81, -89, 84, 24, -120, 1, -96, -106, -102, -65, 83, -66, -118, 59, -119,
39, 54, 97, 69, -31, -109, 115, -14, 8, 15, 124, -107, -17, 27, 42, 113, 123, 125, 81, 46,
102, 28, 8, -84, -68, 116, 78, -26, 80, 77, 5, -79, 104, -48, -48, 70, -39, 124, -88, -38,
103, 37, -107, -70, -94, 28, -6, 105, 7, 34, 89, 67, -86, -90, -74, 106, 82, -51, 65, 125,
-110, 13, 27, -99, 43, 2, -17, -87, -60, -51, -124, -30, 99, -104, -88, -63, -12, 20, -93, -90,
96, 111, -80, -106, 20, 117, -3, -35, -97, -65, 54, -13, 29, 21, 8, -9, -3, -14, -122, -68,
17, 127, 50, 15, 51, -49, 49, 85, -109, 73, -79, 51, -81, 94, -103, 96, 21, 123, -126, 66,
86, 10, 64, 4, 12, -107, -72, -70, -50, -112, -57, 0, 47, -24, 123, 66, 46, 50, 80, -19,
40, 121, -59, 20, -31, -20, 102, 0, 121, 1, -48, -14, -72, 83, -7, 12, 81, -121, -79, -106,
67, 76, -71, -18, 94, -73, 85, 54, 34, 19, -122, 115, -102, 23, 16, -12, 114, 92, 73, 1,
22, 26, -24, 110, -26, 0, -114, 89, 17, -61, -32, -61, 22, 4, 2, -127, -30, -31, 125, -9,
-48, -117, -79, 103, -13, -79, -48, 68, -57, 123, -46, 25, -101, 19, -109, 57, -38, -41, -40, -127,
64, 16, -127, 70, -113, -34, 63, 104, 105, -52, -7, 2, -84, -76, -48, -42, 69, 26, -42, 121,
113, 59, 89, 93, -35, -67, 8, -126, -39, -87, 81, -35, 103, -69, 111, -24, -71, -24, 11, -123,
12, -45, -58, -37, -81, -20, -61, -20, -74, 18, 11, -23, -19, -125, 102, 20, -76, 36, 107, 121,
76, -68, 119, -24, -103, -88, 75, -123, -116, -16, -111, 38, 51, 122, -66, 121, -4, -116, 95, 68,
-42, 59, -5, 8, -126, 24, 54, -24, 120, 68, -124, 103, -24, -23, -88, -14, -29, -40, -24, -28,
-75, 85, -92, -47, -27, 117, 48, -102, -27, -20, 86, 121, 28, 5, 29, 107, 119, 112, 111, -10,
24, 5, -51, -72, 17, -122, -32, 107, -39, -110, -99, 30, -22, -58, -108, -3, -76, -6, 20, -93,
-49, -78, -59, -102, 17, -50, 40, -126, -47, -115, 66, 59, 94, -29, 78, 80, -86, -40, 74, -108,
20, -119, 34, 50, -88, -13, 83, 58, -81, -112, 73, 47, 70, -116, 52, -104, -34, 120, 86, 41,
-119, 4, 10, 1, 24, -26, -106, 71, 88, 10, 41, 0, 60, -93, -47, 31, -107, 18, -124, -123,
-106, -19, -30, 7, 31, 122, -68, 64, 83, 19, 75, -75, -12, 51, 108, -101, -127, -6, -40, -4,
97, -92, -110, -20, 67, 18, -109, -40, -58, 106, -113, -86, -66, 96, 83, -36, 15, -2, 34, -96,
-110, 88, 8, 84, -8, 60, -37, 60, 67, -43, -18, -127, 2, 1, 26, -74, 120, -124, 70, 11,
20, -108, 64, -113, 104, -119, 88, -99, -24, 112, -105, -71, 112, 117, -44, 22, 88, -90, 32, 8,
-27, 48, 33, 76, 119, 78, -52, 89, 101, 20, -100, -12, 49, 21, 24, 84, 97, 12, 59, 19,
46, -44, -36, 75, 65, 70, 70, 118, -83, -2, 67, 101, -21, 80, -123, -65, -69, -64, -79, -68,
127, -48, -106, 4, -6, -113, -37, 111, 38, -57, 11, 112, 71, 102, 113, -50, 0, 0, 0, 0,
73, 69, 78, 68, -82, 66, 96, -126};
private static final byte[] UPDIR = {
-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,
0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,
68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,
0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -111, 73, 68, 65, 84,
88, -123, -19, -106, -51, 74, -21, 64, 20, -57, -1, -87, -47, 7, -15, 41, 92, 118, -47, 82,
10, -59, 110, 20, -95, 32, 77, -101, 44, -92, -37, -66, 80, -23, 3, 8, -126, -76, -48, -115,
-120, 43, 117, -19, -109, -44, -113, -50, -57, -103, 115, 87, 103, 76, 106, -117, 77, 19, -17, -107,
75, -2, 48, -52, -112, 100, 38, -65, 57, -25, -52, 57, 3, 84, -86, 84, -87, -46, -1, -83,
36, 73, 56, 73, 18, -2, -87, -11, -61, 50, 22, -119, -29, -104, 47, 47, 47, 97, -116, -127,
-75, -106, 39, -109, 73, 80, -58, -70, 105, 21, 94, 48, -114, 99, 78, -110, 4, 68, 4, -91,
20, -116, 49, 120, 121, 121, -63, 120, 60, 46, 21, -74, 86, 100, -14, 112, 56, -28, -85, -85,
43, 0, -128, -75, 22, -50, 57, 48, 51, -114, -113, -113, -47, -21, -11, 74, 13, -125, -67, 65,
-121, -61, 33, -113, 70, 35, 4, 65, 0, 99, -116, 111, -42, 90, 16, 17, 58, -99, 14, 46,
46, 46, 74, -125, -35, 11, 84, 44, -23, -100, -61, -57, -57, 7, 86, -85, 21, -116, 49, 32,
34, 111, 89, 107, 45, 90, -83, 22, -50, -50, -50, 74, -127, -51, 125, -104, 6, -125, 1, -57,
113, -20, -83, -89, -108, -126, -75, 22, 0, -32, -100, -13, 45, 8, 2, 48, 51, -102, -51, 38,
-100, 115, 124, 125, 125, 93, 40, 102, 115, -127, 14, 6, 3, -114, -94, -56, -69, 87, 107, 13,
34, 2, 0, -1, 44, 8, -78, 60, 68, -124, 70, -93, 1, 34, -30, -101, -101, -101, -67, 97,
119, 6, -115, -94, -120, 123, -67, -98, -121, 19, 43, 10, -88, -12, -58, 24, 56, -25, -4, 55,
-78, -127, 122, -67, 14, 34, -30, -37, -37, -37, -67, 96, 119, -102, 20, 69, 17, 119, -69, 93,
15, 98, -83, -59, -63, -63, 1, 0, 32, 12, -61, 12, -88, -124, -61, -37, -37, 27, -34, -33,
-33, -3, -122, 68, 15, 15, 15, -104, -49, -25, -71, 97, -65, -99, -48, -17, -9, -71, -35, 110,
-5, 83, 45, 58, 58, 58, -14, -112, 34, -79, -30, -21, -21, 43, -106, -53, 37, -76, -42, 0,
0, -26, -49, -13, 20, -122, 33, -18, -17, -17, -79, 88, 44, 114, -63, 126, -21, 122, 102, -58,
108, 54, -13, 22, 19, 32, 0, 56, 61, 61, -3, -14, 76, 107, -115, -43, 106, 5, 34, -62,
-45, -45, 83, -26, -35, -6, -72, 84, -48, -23, 116, -70, 113, -25, -25, -25, -25, -20, -100, -53,
-4, 60, 29, -109, 114, -6, 103, -77, 89, 41, 21, 106, -17, 90, 47, -112, 2, 8, -64, 3,
-118, -85, -45, -33, 20, -43, -34, -96, -52, -100, 57, -35, -64, 103, -116, -82, 3, -1, 83, 80,
-87, 62, 2, -72, -98, 79, 5, -74, 44, -107, 2, 42, 112, 0, 50, -15, -7, 43, 64, -103,
121, 35, -24, -81, 118, -67, -28, -41, -76, -53, -91, -43, -21, 117, -106, 119, -23, 94, -58, -113,
-113, -113, 59, 101, -123, -62, -96, -101, -54, -87, 88, -14, -28, -28, 4, -121, -121, -121, 25, 48,
-71, 35, 40, -91, -16, -4, -4, -68, -13, -1, 10, -71, 62, 125, -75, 91, 7, -83, -43, 106,
30, 74, 42, -104, -28, 89, -83, 53, -76, -42, -71, 98, -72, -112, 69, -91, 68, 110, -69, -96,
-92, 123, -7, 70, 41, 5, -91, 84, -18, 10, 85, 10, 104, 58, 70, -45, 125, -6, -80, 73,
47, 22, -51, -85, 66, -82, 87, 74, 1, -128, -17, 55, -127, 109, 2, -1, -85, -96, 98, -47,
-76, 91, -73, 1, -82, -113, 1, -8, 107, -30, -113, -125, -34, -35, -35, 5, 68, -28, 19, -27,
-74, -12, 83, 102, -46, -81, 84, -87, 82, -91, 95, -88, 63, 49, -122, -88, 68, 127, -55, -90,
73, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126};
private static final byte[] ARROW = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 48,
0, 0, 0, 117, 8, 3, 0, 0, 0, 63, 73, -110, 106, 0, 0, 2, -9, 80, 76, 84,
69, -46, -46, -46, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -65, -65, -65, 0,
0, 0, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -54, -54, -54,
-48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -49, -49, -49, -86, -86, -86, -48, -48,
-48, -48, -48, -48, -47, -47, -47, -52, -52, -52, -49, -49, -49, -48, -48, -48, -48, -48, -48, -1,
-1, -1, -49, -49, -49, -48, -48, -48, -48, -48, -48, -51, -51, -51, -50, -50, -50, -48, -48, -48,
-48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, -52, -52, -48, -48,
-48, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -52, -52, -52, -48, -48, -48, -48,
-48, -48, -1, -1, -1, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48,
-48, -48, -48, -41, -41, -41, -48, -48, -48, -49, -49, -49, -43, -43, -43, -47, -47, -47, -49, -49,
-49, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, -47,
-47, -47, -49, -49, -49, -47, -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49,
-47, -47, -47, -44, -44, -44, -48, -48, -48, -48, -48, -48, -1, -1, -1, -46, -46, -46, -48, -48,
-48, -49, -49, -49, -47, -47, -47, -49, -49, -49, -35, -35, -35, -49, -49, -49, -49, -49, -49, -49,
-49, -49, -47, -47, -47, -40, -40, -40, -49, -49, -49, -48, -48, -48, -48, -48, -48, -47, -47, -47,
-47, -47, -47, -47, -47, -47, -48, -48, -48, -60, -60, -60, -53, -53, -53, -48, -48, -48, -46, -46,
-46, -65, -65, -65, -48, -48, -48, -54, -54, -54, -45, -45, -45, -47, -47, -47, -49, -49, -49, -48,
-48, -48, -49, -49, -49, -51, -51, -51, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47,
-47, -47, -47, -37, -37, -37, -47, -47, -47, -49, -49, -49, -50, -50, -50, -48, -48, -48, -128, -128,
-128, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47,
-47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, -47, -47, -48, -48, -48,
-50, -50, -50, -49, -49, -49, -48, -48, -48, -48, -48, -48, -33, -33, -33, -48, -48, -48, -52, -52,
-52, -51, -51, -51, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52,
-52, -52, -48, -48, -48, -46, -46, -46, -48, -48, -48, -37, -37, -37, -29, -29, -29, -48, -48, -48,
-48, -48, -48, -47, -47, -47, -48, -48, -48, -49, -49, -49, -46, -46, -46, -48, -48, -48, -49, -49,
-49, -47, -47, -47, -58, -58, -58, -49, -49, -49, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47,
-47, -47, -50, -50, -50, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48,
-48, -48, -48, -55, -55, -55, -45, -45, -45, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48,
-48, -46, -46, -46, -47, -47, -47, -48, -48, -48, -47, -47, -47, -51, -51, -51, -47, -47, -47, -51,
-51, -51, -48, -48, -48, -47, -47, -47, -48, -48, -48, -65, -65, -65, -48, -48, -48, -50, -50, -50,
-48, -48, -48, -47, -47, -47, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -48, -48,
-48, -50, -50, -50, -47, -47, -47, -49, -49, -49, -49, -49, -49, -47, -47, -47, -48, -48, -48, -47,
-47, -47, -49, -49, -49, -39, -39, -39, -50, -50, -50, -47, -47, -47, -48, -48, -48, -42, -42, -42,
-47, -47, -47, -46, -46, -46, -47, -47, -47, -47, -47, -47, -48, -48, -48, -49, -49, -49, -49, -49,
-49, -47, -47, -47, -48, -48, -48, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, -47, -47, -48,
-48, -48, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -50, -50, -50, -47, -47, -47,
-56, -56, -56, -48, -48, -48, -48, -48, -48, -40, -40, -40, -45, -45, -45, -47, -47, -47, -47, -47,
-47, -49, -49, -49, -48, -48, -48, -43, -43, -43, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47,
-47, -47, -50, -50, -50, -49, -49, -49, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49,
-115, 52, -27, 40, 0, 0, 0, -3, 116, 82, 78, 83, 34, -37, -1, -7, 120, 4, 0, 5,
118, -9, -41, 38, 24, -46, -2, -100, 78, 69, 3, -98, -54, 28, 40, -37, -8, 115, 3, 122,
-6, -43, 36, 31, -49, -109, 78, 70, -93, -50, 20, 43, -38, -10, 113, 123, 30, -45, -115, 2,
86, -5, -9, 65, -94, -55, 19, -29, 111, 6, 127, 32, -39, -3, -114, 92, -4, 58, -86, -63,
22, -32, 109, -126, -5, -51, 36, -44, -121, 1, 90, -11, 59, -79, -59, 15, 48, -11, 106, -123,
26, 37, -40, 98, -3, -14, -80, -61, 13, 44, -25, 103, 8, -120, 29, 35, -35, -4, -127, 101,
46, -76, -71, 17, -24, 99, 7, -118, -58, 42, 124, 2, 103, -13, 47, -67, -65, 12, 55, -27,
-116, -57, 24, 44, -35, 119, 112, -21, 49, 8, -70, 10, 51, -19, -14, 93, -111, -62, 25, -30,
119, -78, 14, 9, -108, 113, 117, -18, -53, 63, -23, -15, 89, 9, -106, -2, -66, 21, 50, -125,
-33, -28, 57, 97, -97, -73, 19, 69, -12, -17, 81, -87, 57, 94, -99, -73, 72, -18, 82, -88,
-20, 27, 12, -100, 67, -16, 83, 16, -91, -122, 54, -26, 104, -101, 64, 85, 11, -82, 100, -101,
20, 84, -47, 125, 31, -102, 62, -57, -88, -125, 107, -102, -69, -15, -53, -77, 52, -31, -105, 66,
-21, 87, 121, 110, -67, 23, -60, -84, 13, 46, 111, 88, -75, 119, 42, 50, -32, 106, -107, 63,
91, -78, 117, 114, -108, 41, -51, -28, -65, 0, 0, 3, -107, 73, 68, 65, 84, 88, -61, -107,
-40, 105, 84, -108, 85, 24, 7, 112, -4, -113, -13, -112, -37, -101, -111, -96, 78, 67, -94, 38,
-18, -66, 14, 46, 52, -38, -28, -96, 34, 90, -67, -31, -106, 75, -114, 70, 74, 42, 90, 86,
-109, 4, 38, -88, -111, -90, 50, -126, -72, -46, -54, -102, 38, -102, -53, 104, -101, 123, -88, -71,
78, -18, -91, -90, 88, 82, 46, 89, 90, -71, -107, -27, 7, 95, 62, 120, -114, -100, -93, -121,
-5, 127, 63, -65, -65, 15, -9, -36, 123, -97, -5, 127, -98, -96, -96, 106, -80, 84, -73, -118,
-14, 23, 20, -4, 64, 13, -44, -84, 69, 0, -87, 93, 7, -38, -125, 12, -112, -70, 22, -32,
33, 6, -124, 60, 12, -44, 11, 37, -128, -124, -43, 71, -125, -122, 33, 4, -80, 61, 98, 71,
-8, -93, 4, -112, 70, 17, -48, 26, 51, 64, -102, 0, 53, -102, 50, 64, 30, -45, -48, 44,
-110, 1, -51, 91, -96, 101, 43, 43, 1, -84, -83, -19, -88, -39, -122, 0, 18, -38, 22, 104,
-89, 19, 64, -38, 59, 16, -43, -127, 1, -42, -114, 64, -89, -50, 4, -112, -80, 104, 68, 61,
-82, 19, -64, -39, -59, -126, -120, -82, 4, -112, -48, 39, -32, 122, -110, 1, -46, -51, 1, 119,
12, 3, -84, -35, -127, 30, 61, 9, 32, -51, 99, 17, -43, 75, 39, 64, 112, 92, 111, -12,
105, 68, 0, 121, -22, 105, 104, -49, 48, -64, 120, 22, -120, -17, 75, 0, -111, 126, 64, -1,
1, 12, 24, 24, -117, -25, 6, -39, 8, -32, 28, -20, 64, -77, 33, 4, -112, -95, -49, 3,
113, 6, 1, 100, -104, 7, -61, 99, 24, -32, 28, 1, -68, -112, 64, 0, 121, 113, 36, -30,
71, -23, 4, 72, 124, -55, -114, 78, -93, 9, 32, 99, -58, 34, 105, 28, 3, 100, -68, 27,
13, 94, 102, -128, -13, 21, 96, -62, -85, 4, -112, -127, -81, 33, -2, 117, -125, 0, -34, 55,
-110, 48, 49, -108, 0, -110, -36, 2, -82, 55, 25, -112, -110, 10, 76, 122, -117, 0, 34, -109,
-127, -76, 116, 6, 76, -103, -118, 73, -61, 116, 2, 120, -89, -71, -15, 118, 6, 1, -28, -99,
-23, -64, 12, -125, 0, -58, -69, -64, -52, 89, 4, -112, -39, -26, -70, 51, 125, 4, -112, 57,
89, -56, -98, -101, 66, 0, 95, -114, 3, -13, -26, 19, 64, -110, 23, -64, -79, -112, 1, 82,
-35, -123, -8, -95, 12, -16, -103, -21, 94, -76, -104, 0, 50, 101, 38, -122, 119, 51, 8, -112,
-5, -98, 11, 105, -75, 9, 80, 81, 58, -19, -17, 51, 64, -1, 0, -8, 112, 12, 1, -60,
-6, -111, -71, -18, -39, 4, -112, -113, -13, -112, 95, -112, 66, -128, -62, 34, -83, -8, -109, 37,
4, -112, -91, -79, -16, -92, 50, -64, -8, 20, 88, -58, 0, -55, 1, 74, 24, -80, 124, 36,
44, 43, 8, -112, -16, -103, -85, 120, -27, 42, 2, -84, 94, 3, -1, 90, 67, 29, -92, -41,
1, -42, 89, -43, 55, -50, 54, 24, -120, 14, 34, -114, -58, -25, 37, 112, 127, 65, 28, 62,
-33, -105, -59, -38, 87, -99, 9, -16, -11, 122, -8, 55, 16, 23, -56, 23, 14, 108, -12, -86,
95, 81, 91, 28, -80, 105, 51, 81, 4, -52, 10, 110, 73, 37, -54, -52, -106, -83, 46, -84,
-116, 36, -64, 55, 107, 80, -70, -115, -88, -83, -37, -21, 1, 59, -104, 98, -4, -83, -122, -84,
-47, 68, -71, -33, -71, 11, -10, -35, -60, 11, -108, -66, -57, -115, -52, 72, 2, -20, -51, -122,
127, -97, -95, 14, 2, 17, -64, 119, 78, -11, 103, 87, -17, 98, -34, -29, 90, -60, -61, -66,
60, 26, -98, -3, 68, 116, -16, 29, -48, -76, -52, -125, 4, 88, -67, 9, -2, 67, 68, -4,
-15, 30, 6, -70, 39, -86, 7, 44, -37, 17, 32, -21, 40, 17, -31, 118, -106, -64, 50, -120,
8, -119, -127, -17, 93, -104, -80, -124, 0, 63, 100, 35, -65, 61, -111, -116, -73, 31, 3, -114,
7, 8, 112, -62, 60, -43, 109, -120, -80, -66, 116, 42, -20, 63, 18, -3, 67, -32, -92, 27,
-89, 14, 18, -96, 111, -39, -67, 79, -11, -3, -128, -17, 52, -16, -109, 83, -67, 105, -46, -51,
61, -34, 21, 70, -76, 101, 63, -97, -127, 103, 60, -47, -8, 21, -106, 87, 122, -110, -85, 6,
-65, -4, -118, -46, -77, -122, 58, -16, -98, -82, 28, 43, -86, 2, 33, -25, -52, -32, 114, -108,
104, -64, -25, -100, -127, -3, 60, -47, -30, -5, -54, 93, -72, -16, 27, 1, 98, -4, -56, 46,
32, -26, 26, 9, 23, -127, -33, -1, 32, -64, 17, 13, 101, 42, 73, -27, -50, 63, -105, 74,
-48, -5, 50, 49, -3, 9, 20, 37, -95, -86, 61, -82, 4, 98, 74, -111, -1, -89, -95, 14,
114, -51, 123, -4, 87, -94, -6, -56, -53, -8, 27, -56, -69, 66, 12, -43, -82, 94, -125, -25,
58, 49, -74, 43, -68, -95, 21, 87, 57, -109, -71, 27, -4, -109, -121, -78, 127, 83, -44, -127,
-43, 124, -113, 111, 22, -118, 50, -48, -1, 3, -4, -86, -109, -54, 10, 80, 17, -8, -1, 23,
117, -112, 123, -53, 108, 41, 50, -44, -63, 109, -80, -19, 79, -78, 17, 39, 44, -102, 0, 0,
0, 0, 73, 69, 78, 68, -82, 66, 96, -126};
}