Commit 1a114901 authored by Geoffrey Métais's avatar Geoffrey Métais Committed by Geoffrey Métais

TV: group files & directories

parent 821a1b04
Pipeline #17380 passed with stage
in 4 minutes and 8 seconds
......@@ -94,7 +94,8 @@ class FileBrowserTvFragment : BaseBrowserTvFragment<MediaLibraryItem>(), PathAda
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(viewModel.provider as BrowserProvider).dataset.observe(viewLifecycleOwner, Observer { items ->
(viewModel as BrowserModel).dataset.observe(viewLifecycleOwner, Observer { items ->
if (items == null) return@Observer
val lm = binding.list.layoutManager as LinearLayoutManager
val selectedItem = lm.focusedChild
submitList(items)
......
......@@ -48,15 +48,12 @@ import org.videolan.tools.DependencyProvider
import org.videolan.tools.Settings
import org.videolan.tools.livedata.LiveDataset
import org.videolan.vlc.R
import org.videolan.vlc.util.ModelsHelper
import org.videolan.vlc.util.isBrowserMedia
import org.videolan.vlc.util.isMedia
import org.videolan.vlc.util.*
const val TAG = "VLC/BrowserProvider"
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<MediaLibraryItem>, val url: String?, private var showHiddenFiles: Boolean) : CoroutineScope, HeaderProvider() {
override val coroutineContext = Dispatchers.Main.immediate + SupervisorJob()
......@@ -73,6 +70,13 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
val descriptionUpdate = MutableLiveData<Pair<Int, String>>()
internal val medialibrary = Medialibrary.getInstance()
var desc : Boolean? = null
private val comparator : Comparator<MediaLibraryItem>?
get() = when(desc) {
true -> tvDescComp
false -> tvAscComp
else -> null
}
init {
registerCreator { CoroutineContextProvider() }
......@@ -144,6 +148,7 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
discoveryJob = launch(coroutineContextProvider.Main) { filesFlow(url).collect { findMedia(it)?.let { item -> addMedia(item) } } }
} else {
val files = filesFlow(url).mapNotNull { findMedia(it) }.onEach { addMedia(it) }.toList()
comparator?.let { files.apply { (this as MutableList).sortWith(it) } }
computeHeaders(files)
parseSubDirectories(files)
}
......@@ -180,7 +185,7 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
loading.postValue(false)
}
private suspend fun filesFlow(url: String? = this.url, interact : Boolean = true) = channelFlow {
private suspend fun filesFlow(url: String? = this.url, interact : Boolean = true) = channelFlow<IMedia> {
val listener = object : EventListener {
override fun onMediaAdded(index: Int, media: IMedia) {
if (!isClosedForSend) offer(media.apply { retain() })
......@@ -196,7 +201,9 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
awaitClose { if (url != null) mediabrowser?.changeEventListener(null) }
}.buffer(Channel.UNLIMITED)
open fun addMedia(media: MediaLibraryItem) = dataset.add(media)
open fun addMedia(media: MediaLibraryItem) {
comparator?.let { dataset.add(media, it) } ?: dataset.add(media)
}
open fun refresh() {
if (url === null) return
......@@ -238,14 +245,14 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
if (!isActive) break@loop
//skip media that are not browsable
val item = currentMediaList[currentParsedPosition]
val current = when {
item.itemType == MediaLibraryItem.TYPE_MEDIA -> {
val current = when (item.itemType) {
MediaLibraryItem.TYPE_MEDIA -> {
val mw = item as MediaWrapper
if (mw.type != MediaWrapper.TYPE_DIR && mw.type != MediaWrapper.TYPE_PLAYLIST) continue@loop
if (mw.uri.scheme == "otg" || mw.uri.scheme == "content") continue@loop
mw
}
item.itemType == MediaLibraryItem.TYPE_STORAGE ->
MediaLibraryItem.TYPE_STORAGE ->
MLServiceLocator.getAbstractMediaWrapper((item as Storage).uri).apply { type = MediaWrapper.TYPE_DIR }
else -> continue@loop
}
......@@ -267,6 +274,7 @@ abstract class BrowserProvider(val context: Context, val dataset: LiveDataset<Me
descriptionUpdate.value = Pair(position, it)
}
directories.addAll(files)
comparator?.let { directories.sortWith(it) }
withContext(coroutineContextProvider.Main) { foldersContentMap.put(item, directories.toMutableList()) }
}
directories.clear()
......
......@@ -25,6 +25,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.toCollection
import org.videolan.libvlc.Media
import org.videolan.libvlc.interfaces.IMedia
import org.videolan.libvlc.util.AndroidUtil
......@@ -214,4 +215,4 @@ val View.scope : CoroutineScope
fun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope): Job = scope.launchWhenStarted {
collect() // tail-call
}
\ No newline at end of file
}
......@@ -236,4 +236,50 @@ interface SortModule {
NbMedia -> canSortByMediaNumber()
else -> false
}
}
val ascComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.itemType == MediaLibraryItem.TYPE_MEDIA) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item1?.title?.toLowerCase()?.compareTo(item2?.title?.toLowerCase() ?: "") ?: -1
}
}
val descComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.itemType == MediaLibraryItem.TYPE_MEDIA) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item2?.title?.toLowerCase()?.compareTo(item1?.title?.toLowerCase() ?: "") ?: -1
}
}
val tvAscComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.title?.get(0)?.toLowerCase() == item2?.title?.get(0)?.toLowerCase()) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item1?.title?.toLowerCase()?.compareTo(item2?.title?.toLowerCase() ?: "") ?: -1
}
}
val tvDescComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.title?.get(0)?.toLowerCase() == item2?.title?.get(0)?.toLowerCase()) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item2?.title?.toLowerCase()?.compareTo(item1?.title?.toLowerCase() ?: "") ?: -1
}
}
\ No newline at end of file
......@@ -25,6 +25,7 @@ import androidx.annotation.MainThread
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
......@@ -33,8 +34,10 @@ import kotlinx.coroutines.withContext
import org.videolan.medialibrary.interfaces.media.MediaWrapper
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.tools.CoroutineContextProvider
import org.videolan.tools.Settings
import org.videolan.vlc.providers.*
import org.videolan.vlc.repository.DirectoryRepository
import org.videolan.vlc.util.*
import org.videolan.vlc.viewmodels.BaseModel
import org.videolan.vlc.viewmodels.tv.TvBrowserModel
......@@ -45,15 +48,28 @@ const val TYPE_STORAGE = 3L
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
open class BrowserModel(context: Context, val url: String?, val type: Long, showHiddenFiles: Boolean, private val showDummyCategory: Boolean, coroutineContextProvider: CoroutineContextProvider = CoroutineContextProvider()) : BaseModel<MediaLibraryItem>(context, coroutineContextProvider), TvBrowserModel<MediaLibraryItem>, IPathOperationDelegate by PathOperationDelegate() {
open class BrowserModel(
context: Context,
val url: String?,
val type: Long,
showHiddenFiles: Boolean,
private val showDummyCategory: Boolean,
coroutineContextProvider: CoroutineContextProvider = CoroutineContextProvider()
) : BaseModel<MediaLibraryItem>(
context, coroutineContextProvider),
TvBrowserModel<MediaLibraryItem>,
IPathOperationDelegate by PathOperationDelegate()
{
override var currentItem: MediaLibraryItem? = null
override var nbColumns: Int = 0
private val tv = Settings.showTvUi
override val provider: BrowserProvider = when (type) {
TYPE_PICKER -> FilePickerProvider(context, dataset, url)
TYPE_NETWORK -> NetworkProvider(context, dataset, url, showHiddenFiles)
TYPE_PICKER -> FilePickerProvider(context, dataset, url).also { if (tv) it.desc = desc }
TYPE_NETWORK -> NetworkProvider(context, dataset, url, showHiddenFiles).also { if (tv) it.desc = desc }
TYPE_STORAGE -> StorageProvider(context, dataset, url, showHiddenFiles)
else -> FileBrowserProvider(context, dataset, url, showHiddenFiles = showHiddenFiles, showDummyCategory = showDummyCategory)
else -> FileBrowserProvider(context, dataset, url, showHiddenFiles = showHiddenFiles, showDummyCategory = showDummyCategory).also { if (tv) it.desc = desc }
}
override val loading = provider.loading
......@@ -67,7 +83,13 @@ open class BrowserModel(context: Context, val url: String?, val type: Long, show
viewModelScope.launch {
this@BrowserModel.sort = sort
desc = !desc
dataset.value = withContext(coroutineContextProvider.Default) { dataset.value.apply { sortWith(if (desc) descComp else ascComp) }.also { provider.computeHeaders(dataset.value) } }
if (tv) provider.desc = desc
val comp = if (tv) {
if (desc) tvDescComp else tvAscComp
} else {
if (desc) descComp else ascComp
}
dataset.value = withContext(coroutineContextProvider.Default) { dataset.value.apply { sortWith(comp) }.also { provider.computeHeaders(dataset.value) } }
}
}
......@@ -108,29 +130,6 @@ open class BrowserModel(context: Context, val url: String?, val type: Long, show
override fun canSortByFileNameName(): Boolean = true
}
private val ascComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.itemType == MediaLibraryItem.TYPE_MEDIA) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item1?.title?.toLowerCase()?.compareTo(item2?.title?.toLowerCase() ?: "") ?: -1
}
}
private val descComp by lazy {
Comparator<MediaLibraryItem> { item1, item2 ->
if (item1?.itemType == MediaLibraryItem.TYPE_MEDIA) {
val type1 = (item1 as MediaWrapper).type
val type2 = (item2 as MediaWrapper).type
if (type1 == MediaWrapper.TYPE_DIR && type2 != MediaWrapper.TYPE_DIR) return@Comparator -1
else if (type1 != MediaWrapper.TYPE_DIR && type2 == MediaWrapper.TYPE_DIR) return@Comparator 1
}
item2?.title?.toLowerCase()?.compareTo(item1?.title?.toLowerCase() ?: "") ?: -1
}
}
@ExperimentalCoroutinesApi
fun Fragment.getBrowserModel(category: Long, url: String?, showHiddenFiles: Boolean, showDummyCategory: Boolean = false) = if (category == TYPE_NETWORK)
ViewModelProvider(this, NetworkModel.Factory(requireContext(), url, showHiddenFiles)).get(NetworkModel::class.java)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment