Browse Source

Allow folder creation from file/folder choosers. Resolves #1602.

Aidan Follestad 6 years ago
parent
commit
00ab139f08

+ 2 - 0
files/build.gradle

@@ -28,7 +28,9 @@ android {
 dependencies {
   implementation 'com.android.support:recyclerview-v7:' + versions.supportLib
   implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
+
   implementation project(':core')
+  implementation project(':input')
 }
 
 apply from: '../spotless.gradle'

+ 34 - 1
files/src/main/java/com/afollestad/materialdialogs/files/DialogFileChooserExt.kt

@@ -8,6 +8,7 @@ package com.afollestad.materialdialogs.files
 import android.annotation.SuppressLint
 import android.os.Environment.getExternalStorageDirectory
 import android.support.annotation.CheckResult
+import android.support.annotation.StringRes
 import android.support.v7.widget.LinearLayoutManager
 import android.widget.TextView
 import com.afollestad.materialdialogs.MaterialDialog
@@ -16,7 +17,10 @@ import com.afollestad.materialdialogs.actions.setActionButtonEnabled
 import com.afollestad.materialdialogs.customview.customView
 import com.afollestad.materialdialogs.customview.getCustomView
 import com.afollestad.materialdialogs.files.utilext.hasReadStoragePermission
+import com.afollestad.materialdialogs.files.utilext.hasWriteStoragePermission
 import com.afollestad.materialdialogs.files.utilext.maybeSetTextColor
+import com.afollestad.materialdialogs.files.utilext.updatePadding
+import com.afollestad.materialdialogs.input.input
 import com.afollestad.materialdialogs.internal.list.DialogRecyclerView
 import java.io.File
 
@@ -41,9 +45,13 @@ fun MaterialDialog.fileChooser(
   filter: FileFilter = { !it.isHidden },
   waitForPositiveButton: Boolean = true,
   emptyTextRes: Int = R.string.files_default_empty_text,
+  allowFolderCreation: Boolean = false,
+  @StringRes folderCreationLabel: Int? = null,
   selection: FileCallback = null
 ): MaterialDialog {
-  if (!hasReadStoragePermission()) {
+  if (allowFolderCreation && !hasWriteStoragePermission()) {
+    throw IllegalStateException("You must have the WRITE_EXTERNAL_STORAGE permission first.")
+  } else if (!hasReadStoragePermission()) {
     throw IllegalStateException("You must have the READ_EXTERNAL_STORAGE permission first.")
   }
   customView(R.layout.md_file_chooser_base)
@@ -64,6 +72,8 @@ fun MaterialDialog.fileChooser(
       emptyView = emptyText,
       onlyFolders = false,
       filter = filter,
+      allowFolderCreation = allowFolderCreation,
+      folderCreationLabel = folderCreationLabel,
       callback = selection
   )
   list.adapter = adapter
@@ -78,5 +88,28 @@ fun MaterialDialog.fileChooser(
     }
   }
 
+  if (allowFolderCreation) {
+    // Increase empty text top padding to make room for New Folder option
+    emptyText.updatePadding(
+        top = context.resources.getDimensionPixelSize(
+            R.dimen.empty_text_padding_top_larger
+        )
+    )
+  }
+
   return this
 }
+
+internal fun MaterialDialog.showNewFolderCreator(
+  parent: File,
+  @StringRes folderCreationLabel: Int?,
+  onCreation: () -> Unit
+) {
+  MaterialDialog(windowContext).show {
+    title(folderCreationLabel ?: R.string.files_new_folder)
+    input(hintRes = R.string.files_new_folder_hint) { _, input ->
+      File(parent, input.toString().trim()).mkdir()
+      onCreation()
+    }
+  }
+}

+ 9 - 1
files/src/main/java/com/afollestad/materialdialogs/files/DialogFolderChooserExt.kt

@@ -8,6 +8,7 @@ package com.afollestad.materialdialogs.files
 import android.annotation.SuppressLint
 import android.os.Environment.getExternalStorageDirectory
 import android.support.annotation.CheckResult
+import android.support.annotation.StringRes
 import android.support.v7.widget.LinearLayoutManager
 import android.widget.TextView
 import com.afollestad.materialdialogs.MaterialDialog
@@ -16,6 +17,7 @@ import com.afollestad.materialdialogs.actions.setActionButtonEnabled
 import com.afollestad.materialdialogs.customview.customView
 import com.afollestad.materialdialogs.customview.getCustomView
 import com.afollestad.materialdialogs.files.utilext.hasReadStoragePermission
+import com.afollestad.materialdialogs.files.utilext.hasWriteStoragePermission
 import com.afollestad.materialdialogs.files.utilext.maybeSetTextColor
 import com.afollestad.materialdialogs.internal.list.DialogRecyclerView
 import java.io.File
@@ -38,9 +40,13 @@ fun MaterialDialog.folderChooser(
   filter: FileFilter = { !it.isHidden },
   waitForPositiveButton: Boolean = true,
   emptyTextRes: Int = R.string.files_default_empty_text,
+  allowFolderCreation: Boolean = false,
+  @StringRes folderCreationLabel: Int? = null,
   selection: FileCallback = null
 ): MaterialDialog {
-  if (!hasReadStoragePermission()) {
+  if (allowFolderCreation && !hasWriteStoragePermission()) {
+    throw IllegalStateException("You must have the WRITE_EXTERNAL_STORAGE permission first.")
+  } else if (!hasReadStoragePermission()) {
     throw IllegalStateException("You must have the READ_EXTERNAL_STORAGE permission first.")
   }
   customView(R.layout.md_file_chooser_base)
@@ -61,6 +67,8 @@ fun MaterialDialog.folderChooser(
       emptyView = emptyText,
       onlyFolders = true,
       filter = filter,
+      allowFolderCreation = allowFolderCreation,
+      folderCreationLabel = folderCreationLabel,
       callback = selection
   )
   list.adapter = adapter

+ 72 - 11
files/src/main/java/com/afollestad/materialdialogs/files/FileChooserAdapter.kt

@@ -5,6 +5,7 @@
  */
 package com.afollestad.materialdialogs.files
 
+import android.support.annotation.StringRes
 import android.support.v7.widget.RecyclerView
 import android.view.LayoutInflater
 import android.view.View
@@ -50,6 +51,8 @@ internal class FileChooserAdapter(
   private val emptyView: TextView,
   private val onlyFolders: Boolean,
   private val filter: FileFilter,
+  private val allowFolderCreation: Boolean,
+  @StringRes private val folderCreationLabel: Int?,
   private val callback: FileCallback
 ) : RecyclerView.Adapter<FileChooserViewHolder>() {
 
@@ -66,13 +69,30 @@ internal class FileChooserAdapter(
   }
 
   fun itemClicked(index: Int) {
-    if (currentFolder.hasParent() && index == 0) {
+    if (
+        currentFolder.hasParent() &&
+        index == goUpIndex()
+    ) {
       // go up
       loadContents(currentFolder.betterParent()!!)
       return
+    } else if (
+        currentFolder.canWrite() &&
+        allowFolderCreation &&
+        index == newFolderIndex()
+    ) {
+      // New folder
+      dialog.showNewFolderCreator(
+          parent = currentFolder,
+          folderCreationLabel = folderCreationLabel
+      ) {
+        // Refresh view
+        loadContents(currentFolder)
+      }
+      return
     }
 
-    val actualIndex = if (currentFolder.hasParent()) index - 1 else index
+    val actualIndex = actualIndex(index)
     val selected = contents[actualIndex].jumpOverEmulated()
 
     if (selected.isDirectory) {
@@ -117,6 +137,17 @@ internal class FileChooserAdapter(
     notifyDataSetChanged()
   }
 
+  override fun getItemCount(): Int {
+    var count = contents.size
+    if (currentFolder.hasParent()) {
+      count += 1
+    }
+    if (allowFolderCreation && currentFolder.canWrite()) {
+      count += 1
+    }
+    return count
+  }
+
   override fun onCreateViewHolder(
     parent: ViewGroup,
     viewType: Int
@@ -125,6 +156,7 @@ internal class FileChooserAdapter(
       // If we don't have folder chooser action buttons at runtime, force one
       dialog.positiveButton(android.R.string.ok)
     }
+
     val view = LayoutInflater.from(parent.context)
         .inflate(R.layout.md_file_chooser_item, parent, false)
     view.background = getDrawable(dialog.context, attr = R.attr.md_item_selector)
@@ -134,18 +166,15 @@ internal class FileChooserAdapter(
     return viewHolder
   }
 
-  override fun getItemCount(): Int {
-    if (currentFolder.hasParent()) {
-      return contents.size + 1
-    }
-    return contents.size
-  }
-
   override fun onBindViewHolder(
     holder: FileChooserViewHolder,
     position: Int
   ) {
-    if (currentFolder.hasParent() && position == 0) {
+    if (
+        currentFolder.hasParent() &&
+        position == goUpIndex()
+    ) {
+      // Go up
       holder.iconView.setImageResource(
           if (isLightTheme) R.drawable.icon_return_dark
           else R.drawable.icon_return_light
@@ -155,13 +184,45 @@ internal class FileChooserAdapter(
       return
     }
 
-    val actualIndex = if (currentFolder.hasParent()) position - 1 else position
+    if (
+        allowFolderCreation &&
+        currentFolder.canWrite() &&
+        position == newFolderIndex()
+    ) {
+      // New folder
+      holder.iconView.setImageResource(
+          if (isLightTheme) R.drawable.icon_new_folder_dark
+          else R.drawable.icon_new_folder_light
+      )
+      holder.nameView.text = dialog.windowContext.getString(
+          folderCreationLabel ?: R.string.files_new_folder
+      )
+      holder.itemView.isActivated = false
+      return
+    }
+
+    val actualIndex = actualIndex(position)
     val item = contents[actualIndex]
     holder.iconView.setImageResource(item.iconRes())
     holder.nameView.text = item.name
     holder.itemView.isActivated = selectedFile?.absolutePath == item.absolutePath ?: false
   }
 
+  private fun goUpIndex() = if (currentFolder.hasParent()) 0 else -1
+
+  private fun newFolderIndex() = if (currentFolder.hasParent()) 1 else 0
+
+  private fun actualIndex(position: Int): Int {
+    var actualIndex = position
+    if (currentFolder.hasParent()) {
+      actualIndex -= 1
+    }
+    if (currentFolder.canWrite() && allowFolderCreation) {
+      actualIndex -= 1
+    }
+    return actualIndex
+  }
+
   private fun File.iconRes(): Int {
     return if (isLightTheme) {
       if (this.isDirectory) R.drawable.icon_folder_dark

+ 4 - 0
files/src/main/java/com/afollestad/materialdialogs/files/utilext/FilesUtilExt.kt

@@ -58,3 +58,7 @@ internal fun Context.hasPermission(permission: String): Boolean {
 internal fun MaterialDialog.hasReadStoragePermission(): Boolean {
   return windowContext.hasPermission(permission.READ_EXTERNAL_STORAGE)
 }
+
+internal fun MaterialDialog.hasWriteStoragePermission(): Boolean {
+  return windowContext.hasPermission(permission.WRITE_EXTERNAL_STORAGE)
+}

+ 17 - 0
files/src/main/java/com/afollestad/materialdialogs/files/utilext/ViewExt.kt

@@ -10,3 +10,20 @@ import android.view.View
 internal fun <T : View> T.setVisible(visible: Boolean) {
   visibility = if (visible) View.VISIBLE else View.INVISIBLE
 }
+
+internal fun <T : View> T.updatePadding(
+  left: Int = this.paddingLeft,
+  top: Int = this.paddingTop,
+  right: Int = this.paddingRight,
+  bottom: Int = this.paddingBottom
+) {
+  if (left == this.paddingLeft &&
+      top == this.paddingTop &&
+      right == this.paddingRight &&
+      bottom == this.paddingBottom
+  ) {
+    // no change needed, don't want to invalidate layout
+    return
+  }
+  this.setPadding(left, top, right, bottom)
+}

+ 10 - 0
files/src/main/res/drawable/icon_new_folder_dark.xml

@@ -0,0 +1,10 @@
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="36dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:width="36dp">
+  <path
+      android:fillColor="#424242"
+      android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
+</vector>

+ 10 - 0
files/src/main/res/drawable/icon_new_folder_light.xml

@@ -0,0 +1,10 @@
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="36dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:width="36dp">
+  <path
+      android:fillColor="#EEEEEE"
+      android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
+</vector>

+ 1 - 0
files/src/main/res/values/dimens.xml

@@ -3,6 +3,7 @@
   <dimen name="empty_text_size">18sp</dimen>
   <dimen name="empty_text_padding">48dp</dimen>
   <dimen name="empty_text_padding_top">72dp</dimen>
+  <dimen name="empty_text_padding_top_larger">132dp</dimen>
   <dimen name="file_chooser_item_vertical_padding">8dp</dimen>
   <dimen name="file_chooser_item_horizontal_padding">22dp</dimen>
   <dimen name="file_chooser_item_icon_margin_end">12dp</dimen>

+ 2 - 0
files/src/main/res/values/strings.xml

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
   <string name="files_default_empty_text">This folder\'s empty!</string>
+  <string name="files_new_folder">New Folder…</string>
+  <string name="files_new_folder_hint">New folder name</string>
 </resources>

+ 5 - 4
sample/src/main/java/com/afollestad/materialdialogssample/MainActivity.kt

@@ -8,6 +8,7 @@
 package com.afollestad.materialdialogssample
 
 import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
 import android.content.SharedPreferences
 import android.graphics.Color
 import android.os.Bundle
@@ -724,14 +725,14 @@ class MainActivity : AppCompatActivity() {
   }
 
   private fun showFileChooserButtons() {
-    permission.request(arrayOf(READ_EXTERNAL_STORAGE)) { result ->
+    permission.request(arrayOf(WRITE_EXTERNAL_STORAGE)) { result ->
       if (!result.allGranted()) {
         toast("Storage permission is needed for file choosers")
         return@request
       }
 
       MaterialDialog(this).show {
-        fileChooser { _, file ->
+        fileChooser(allowFolderCreation = true) { _, file ->
           toast("Selected file: ${file.absolutePath}")
         }
         negativeButton(android.R.string.cancel)
@@ -758,14 +759,14 @@ class MainActivity : AppCompatActivity() {
   }
 
   private fun showFolderChooserButtons() {
-    permission.request(arrayOf(READ_EXTERNAL_STORAGE)) { result ->
+    permission.request(arrayOf(WRITE_EXTERNAL_STORAGE)) { result ->
       if (!result.allGranted()) {
         toast("Storage permission is needed for file choosers")
         return@request
       }
 
       MaterialDialog(this).show {
-        folderChooser { _, folder ->
+        folderChooser(allowFolderCreation = true) { _, folder ->
           toast("Selected folder: ${folder.absolutePath}")
         }
         negativeButton(android.R.string.cancel)