[Android] Allow adding folder and subfolders to playlist from file browser

Nicolas Pomepuy git at videolan.org
Tue Mar 10 09:36:04 CET 2020


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Fri Mar  6 09:55:14 2020 +0100| [f63eed321e9a9fa8af227311123a8bfa4429b5a1] | committer: Geoffrey Métais

Allow adding folder and subfolders to playlist from file browser

Fixes #1166

> https://code.videolan.org/videolan/vlc-android/commit/f63eed321e9a9fa8af227311123a8bfa4429b5a1
---

 .../resources/src/main/res/values/strings.xml      |  2 ++
 .../vlc-android/res/menu/activity_option.xml       | 17 +++++++++++++
 .../vlc/gui/browser/BaseBrowserFragment.kt         | 21 ++++++++++++++++
 .../org/videolan/vlc/providers/BrowserProvider.kt  | 29 ++++++++++++++++++++--
 .../videolan/vlc/providers/FileBrowserProvider.kt  | 23 +++++++++++++++++
 5 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/application/resources/src/main/res/values/strings.xml b/application/resources/src/main/res/values/strings.xml
index 9a5fcd827..3a329d0e4 100644
--- a/application/resources/src/main/res/values/strings.xml
+++ b/application/resources/src/main/res/values/strings.xml
@@ -804,4 +804,6 @@
     <string name="track_index">Track: %s</string>
     <string name="audio_queue_progress">Progress: %s</string>
     <string name="sortby_track">Track</string>
+    <string name="this_folder">This folder</string>
+    <string name="all_subfolders">This folder and subfolders</string>
 </resources>
diff --git a/application/vlc-android/res/menu/activity_option.xml b/application/vlc-android/res/menu/activity_option.xml
index 578c92a77..52da413c9 100644
--- a/application/vlc-android/res/menu/activity_option.xml
+++ b/application/vlc-android/res/menu/activity_option.xml
@@ -117,6 +117,23 @@
     </item>
     <item
             android:orderInCategory="3"
+            android:title="@string/add_to_playlist"
+            android:id="@+id/ml_menu_add_playlist"
+            vlc:showAsAction="never"
+            android:visible="false">
+        <menu>
+            <item
+                    android:orderInCategory="2"
+                    android:id="@+id/folder_add_playlist"
+                    android:title="@string/this_folder" />
+            <item
+                    android:orderInCategory="2"
+                    android:id="@+id/subfolders_add_playlist"
+                    android:title="@string/all_subfolders" />
+        </menu>
+    </item>
+    <item
+            android:orderInCategory="4"
             android:id="@+id/ml_menu_refresh"
             android:icon="@drawable/ic_menu_refresh"
             vlc:showAsAction="never"
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserFragment.kt b/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserFragment.kt
index 0a12bdfba..7b6e80b6b 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserFragment.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/browser/BaseBrowserFragment.kt
@@ -22,6 +22,7 @@
  */
 package org.videolan.vlc.gui.browser
 
+import android.content.Context
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
@@ -58,17 +59,20 @@ import org.videolan.vlc.gui.dialogs.showContext
 import org.videolan.vlc.gui.helpers.MedialibraryUtils
 import org.videolan.vlc.gui.helpers.UiTools
 import org.videolan.vlc.gui.helpers.hf.OTG_SCHEME
+import org.videolan.vlc.gui.helpers.hf.getDocumentFiles
 import org.videolan.vlc.gui.view.EmptyLoadingState
 import org.videolan.vlc.gui.view.VLCDividerItemDecoration
 import org.videolan.vlc.interfaces.IEventsHandler
 import org.videolan.vlc.interfaces.IRefreshable
 import org.videolan.vlc.media.MediaUtils
 import org.videolan.vlc.media.PlaylistManager
+import org.videolan.vlc.providers.FileBrowserProvider
 import org.videolan.vlc.repository.BrowserFavRepository
 import org.videolan.vlc.util.Permissions
 import org.videolan.vlc.util.isSchemeSupported
 import org.videolan.vlc.viewmodels.browser.BrowserModel
 import java.util.*
+import kotlin.collections.ArrayList
 
 private const val TAG = "VLC/BaseBrowserFragment"
 
@@ -120,6 +124,7 @@ abstract class BaseBrowserFragment : MediaBrowserFragment<BrowserModel>(), IRefr
         super.onPrepareOptionsMenu(menu)
         menu.findItem(R.id.ml_menu_filter)?.isVisible = enableSearchOption()
         menu.findItem(R.id.ml_menu_sortby)?.isVisible = !isRootDirectory
+        menu.findItem(R.id.ml_menu_add_playlist)?.isVisible = !isRootDirectory
         val browserShowAllFiles = menu.findItem(R.id.browser_show_all_files)
         browserShowAllFiles.isVisible = true
         browserShowAllFiles.isChecked = Settings.getInstance(requireActivity()).getBoolean("browser_show_all_files", true)
@@ -429,6 +434,22 @@ abstract class BaseBrowserFragment : MediaBrowserFragment<BrowserModel>(), IRefr
                 }
                 true
             }
+            R.id.folder_add_playlist -> {
+                val medias = viewModel.dataset.value.asSequence().map { it as MediaWrapper }.filter { it.type != MediaWrapper.TYPE_DIR }.toList().toTypedArray()
+                UiTools.addToPlaylist(requireActivity(), medias, SavePlaylistDialog.KEY_NEW_TRACKS)
+                true
+            }
+            R.id.subfolders_add_playlist -> {
+                currentMedia?.let {
+                    lifecycleScope.launchWhenStarted {
+                        val medias = withContext(Dispatchers.IO) {
+                            (viewModel.provider as FileBrowserProvider).browseByUrl(it.uri.toString())
+                        }
+                        UiTools.addToPlaylist(requireActivity(), medias, SavePlaylistDialog.KEY_NEW_TRACKS)
+                    }
+                }
+                true
+            }
             else -> super.onOptionsItemSelected(item)
         }
     }
diff --git a/application/vlc-android/src/org/videolan/vlc/providers/BrowserProvider.kt b/application/vlc-android/src/org/videolan/vlc/providers/BrowserProvider.kt
index 9bf2ef42a..b444f6c50 100644
--- a/application/vlc-android/src/org/videolan/vlc/providers/BrowserProvider.kt
+++ b/application/vlc-android/src/org/videolan/vlc/providers/BrowserProvider.kt
@@ -51,7 +51,6 @@ import org.videolan.vlc.R
 import org.videolan.vlc.util.ModelsHelper
 import org.videolan.vlc.util.isBrowserMedia
 import org.videolan.vlc.util.isMedia
-import java.util.*
 
 const val TAG = "VLC/BrowserProvider"
 
@@ -104,6 +103,7 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
                     mediabrowser?.release()
                     mediabrowser = null
                 }
+                is BrowseUrl -> action.deferred.complete(browseUrlImpl(action.url))
             }
         }
     }
@@ -150,6 +150,28 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
         if (url != null ) loading.postValue(false)
     }
 
+    suspend fun browseUrl(url: String): List<MediaLibraryItem> {
+        val deferred = CompletableDeferred<List<MediaLibraryItem>>()
+        browserActor.post(BrowseUrl(url, deferred))
+        return deferred.await()
+    }
+
+    suspend fun browseUrlImpl(url: String): List<MediaLibraryItem> {
+        val children = filesFlow(url).toList()
+        val medias = ArrayList<MediaLibraryItem>()
+        val directories = ArrayList<MediaWrapper>()
+        children.map { MLServiceLocator.getAbstractMediaWrapper(it) }.forEach {
+            when (it.type) {
+                MediaWrapper.TYPE_AUDIO, MediaWrapper.TYPE_VIDEO -> medias.add(it)
+                MediaWrapper.TYPE_DIR -> directories.add(it)
+            }
+        }
+        directories.forEach {
+            medias.addAll(browseUrlImpl(it.uri.toString()))
+        }
+        return medias.toList()
+    }
+
     protected open suspend fun refreshImpl() {
         val files = filesFlow().mapNotNull { findMedia(it) }.toList()
         dataset.value = files as MutableList<MediaLibraryItem>
@@ -171,7 +193,9 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
             override fun onMediaRemoved(index: Int, media: IMedia) {}
         }
         requestBrowsing(url, listener, interact)
-        awaitClose { if (url != null) browserActor.post(ClearListener) }
+        awaitClose {
+            if (url != null) browserActor.post(ClearListener)
+        }
     }.buffer(Channel.UNLIMITED)
 
     open fun addMedia(media: MediaLibraryItem) = dataset.add(media)
@@ -345,3 +369,4 @@ private object Refresh : BrowserAction()
 private class ParseSubDirectories(val list : List<MediaLibraryItem>? = null) : BrowserAction()
 private object ClearListener : BrowserAction()
 private object Release : BrowserAction()
+private class BrowseUrl(val url: String, val deferred: CompletableDeferred<List<MediaLibraryItem>>) : BrowserAction()
diff --git a/application/vlc-android/src/org/videolan/vlc/providers/FileBrowserProvider.kt b/application/vlc-android/src/org/videolan/vlc/providers/FileBrowserProvider.kt
index 5c71fd66d..a24970298 100644
--- a/application/vlc-android/src/org/videolan/vlc/providers/FileBrowserProvider.kt
+++ b/application/vlc-android/src/org/videolan/vlc/providers/FileBrowserProvider.kt
@@ -167,6 +167,29 @@ open class FileBrowserProvider(
         }
     }
 
+    suspend fun browseByUrl(url: String): Array<MediaWrapper> {
+        return when {
+            url == "otg://" || url.startsWith("content:") -> {
+                val result = ArrayList<MediaWrapper>()
+                launch {
+                    val files = withContext(coroutineContextProvider.IO) {
+                        @Suppress("UNCHECKED_CAST")
+                        getDocumentFiles(context, Uri.parse(url).path?.substringAfterLast(':')
+                                ?: "") as? MutableList<MediaLibraryItem> ?: mutableListOf()
+                    }.map { it as MediaWrapper }
+
+                    result.addAll(files.filter { it.itemType == MediaWrapper.TYPE_MEDIA })
+                    files.filter { it.itemType == MediaWrapper.TYPE_DIR }.forEach {
+                        result.addAll(browseByUrl(it.uri.toString()))
+                    }
+                }
+                result.toList().toTypedArray()
+            }
+
+            else -> super.browseUrl(url).toList().map { it as MediaWrapper }.toTypedArray()
+        }
+    }
+
     override fun release() {
         if (url == null) {
             ExternalMonitor.devices.removeObserver(this)



More information about the Android mailing list