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

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