Implement singular selection for Bookmarks
This commit is contained in:
parent
ad793e27a5
commit
5dfb66eec2
|
@ -108,7 +108,6 @@ dependencies {
|
||||||
implementation other.rxJava
|
implementation other.rxJava
|
||||||
implementation other.rxAndroid
|
implementation other.rxAndroid
|
||||||
implementation other.multiType
|
implementation other.multiType
|
||||||
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
|
|
||||||
|
|
||||||
kapt androidSupport.room
|
kapt androidSupport.room
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,18 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.drakeet.multitype.MultiTypeAdapter
|
import com.drakeet.multitype.MultiTypeAdapter
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
|
import org.moire.ultrasonic.util.BoundedTreeSet
|
||||||
|
|
||||||
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
|
|
||||||
internal var selectedSet: TreeSet<Long> = TreeSet()
|
// Update the BoundedTreeSet if selection type is changed
|
||||||
|
internal var selectionType: SelectionType = SelectionType.MULTIPLE
|
||||||
|
set(newValue) {
|
||||||
|
field = newValue
|
||||||
|
selectedSet.setMaxSize(newValue.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal var selectedSet: BoundedTreeSet<Long> = BoundedTreeSet(selectionType.size)
|
||||||
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
|
internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
|
||||||
|
|
||||||
private val diffCallback = GenericDiffCallback<T>()
|
private val diffCallback = GenericDiffCallback<T>()
|
||||||
|
@ -26,6 +34,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
return getItem(position).longId
|
return getItem(position).longId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getItem(position: Int): T {
|
private fun getItem(position: Int): T {
|
||||||
return mDiffer.currentList[position]
|
return mDiffer.currentList[position]
|
||||||
}
|
}
|
||||||
|
@ -183,10 +192,22 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
list.add(to - 1, fromLocation)
|
list.add(to - 1, fromLocation)
|
||||||
}
|
}
|
||||||
submitList(list)
|
submitList(list)
|
||||||
return list as List<T>
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasSingleSelection(): Boolean {
|
||||||
|
return selectionType == SelectionType.SINGLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasMultipleSelection(): Boolean {
|
||||||
|
return selectionType == SelectionType.MULTIPLE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SelectionType(val size: Int) {
|
||||||
|
SINGLE(1),
|
||||||
|
MULTIPLE(Int.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
/**
|
||||||
* Calculates the differences between data sets
|
* Calculates the differences between data sets
|
||||||
*/
|
*/
|
||||||
|
@ -200,5 +221,4 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a RecyclerView which can be used as a divide between different sections
|
||||||
|
*/
|
||||||
|
class DividerBinder: ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHolder>() {
|
||||||
|
|
||||||
|
|
||||||
|
// Set our layout files
|
||||||
|
val layout = R.layout.row_divider
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, item: Divider) {
|
||||||
|
// Set text
|
||||||
|
holder.textView.setText(item.stringId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup
|
||||||
|
): ViewHolder {
|
||||||
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewHolder class
|
||||||
|
class ViewHolder(
|
||||||
|
itemView: View
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var textView: TextView = itemView.findViewById(R.id.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class to store our data into
|
||||||
|
data class Divider(val stringId: Int): Identifiable {
|
||||||
|
override val id: String
|
||||||
|
get() = stringId.toString()
|
||||||
|
override val longId: Long
|
||||||
|
get() = stringId.toLong()
|
||||||
|
|
||||||
|
override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -276,7 +276,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
|
|
||||||
override fun setChecked(newStatus: Boolean) {
|
override fun setChecked(newStatus: Boolean) {
|
||||||
observableChecked.postValue(newStatus)
|
observableChecked.postValue(newStatus)
|
||||||
check.isChecked = newStatus
|
//check.isChecked = newStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isChecked(): Boolean {
|
override fun isChecked(): Boolean {
|
||||||
|
|
|
@ -7,26 +7,36 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the Bookmarks available on the server
|
* Lists the Bookmarks available on the server
|
||||||
|
*
|
||||||
|
* Bookmarks allows to save the play position of tracks, especially useful for longer tracks like
|
||||||
|
* audio books etc.
|
||||||
|
*
|
||||||
|
* Therefore this fragment allows only for singular selection and playback.
|
||||||
|
*
|
||||||
|
* // FIXME: use restore for playback
|
||||||
*/
|
*/
|
||||||
class BookmarksFragment : TrackCollectionFragment() {
|
class BookmarksFragment : TrackCollectionFragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
setTitle(this, R.string.button_bar_bookmarks)
|
setTitle(this, R.string.button_bar_bookmarks)
|
||||||
|
|
||||||
|
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupButtons(view: View) {
|
override fun setupButtons(view: View) {
|
||||||
super.setupButtons(view)
|
super.setupButtons(view)
|
||||||
|
|
||||||
// Why?
|
// Hide select all button
|
||||||
selectButton?.visibility = View.GONE
|
//selectButton?.visibility = View.GONE
|
||||||
moreButton?.visibility = View.GONE
|
//moreButton?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
||||||
|
@ -37,10 +47,6 @@ class BookmarksFragment : TrackCollectionFragment() {
|
||||||
}
|
}
|
||||||
return listModel.currentList
|
return listModel.currentList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableButtons(selection: List<MusicDirectory.Entry>) {
|
|
||||||
super.enableButtons(selection)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||||
|
import org.moire.ultrasonic.adapters.DividerBinder
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -36,6 +37,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
|
import org.moire.ultrasonic.util.CommunicationError
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
|
@ -147,6 +149,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
viewAdapter.register(
|
||||||
|
DividerBinder()
|
||||||
|
)
|
||||||
|
|
||||||
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
// Fragment was started with a query (e.g. from voice search), try to execute search right away
|
||||||
val arguments = arguments
|
val arguments = arguments
|
||||||
if (arguments != null) {
|
if (arguments != null) {
|
||||||
|
@ -415,10 +421,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search(query: String, autoplay: Boolean) {
|
private fun search(query: String, autoplay: Boolean) {
|
||||||
// FIXME add error handler
|
|
||||||
// FIXME support autoplay
|
// FIXME support autoplay
|
||||||
listModel.viewModelScope.launch {
|
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||||
listModel.search(query)
|
listModel.search(query)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,7 +435,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
|
|
||||||
val artists = searchResult.artists
|
val artists = searchResult.artists
|
||||||
if (artists.isNotEmpty()) {
|
if (artists.isNotEmpty()) {
|
||||||
// FIXME: addView(albumsHeading)
|
|
||||||
|
list.add(DividerBinder.Divider(R.string.search_artists))
|
||||||
list.addAll(artists)
|
list.addAll(artists)
|
||||||
if (artists.size > DEFAULT_ARTISTS) {
|
if (artists.size > DEFAULT_ARTISTS) {
|
||||||
// FIXME
|
// FIXME
|
||||||
|
@ -438,7 +445,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
}
|
}
|
||||||
val albums = searchResult.albums
|
val albums = searchResult.albums
|
||||||
if (albums.isNotEmpty()) {
|
if (albums.isNotEmpty()) {
|
||||||
// mergeAdapter!!.addView(albumsHeading)
|
list.add(DividerBinder.Divider(R.string.search_albums))
|
||||||
list.addAll(albums)
|
list.addAll(albums)
|
||||||
// mergeAdapter!!.addAdapter(albumAdapter)
|
// mergeAdapter!!.addAdapter(albumAdapter)
|
||||||
// if (albums.size > DEFAULT_ALBUMS) {
|
// if (albums.size > DEFAULT_ALBUMS) {
|
||||||
|
@ -447,8 +454,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||||
}
|
}
|
||||||
val songs = searchResult.songs
|
val songs = searchResult.songs
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
// mergeAdapter!!.addView(songsHeading)
|
list.add(DividerBinder.Divider(R.string.search_albums))
|
||||||
|
|
||||||
list.addAll(songs)
|
list.addAll(songs)
|
||||||
// if (songs.size > DEFAULT_SONGS) {
|
// if (songs.size > DEFAULT_SONGS) {
|
||||||
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||||
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
|
@ -431,6 +432,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
||||||
val enabled = selection.isNotEmpty()
|
val enabled = selection.isNotEmpty()
|
||||||
var unpinEnabled = false
|
var unpinEnabled = false
|
||||||
var deleteEnabled = false
|
var deleteEnabled = false
|
||||||
|
val multipleSelection = viewAdapter.hasMultipleSelection()
|
||||||
|
|
||||||
var pinnedCount = 0
|
var pinnedCount = 0
|
||||||
|
|
||||||
|
@ -446,8 +448,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
playNowButton?.isVisible = enabled
|
playNowButton?.isVisible = enabled
|
||||||
playNextButton?.isVisible = enabled
|
playNextButton?.isVisible = enabled && multipleSelection
|
||||||
playLastButton?.isVisible = enabled
|
playLastButton?.isVisible = enabled && multipleSelection
|
||||||
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount)
|
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount)
|
||||||
unpinButton?.isVisible = (enabled && unpinEnabled)
|
unpinButton?.isVisible = (enabled && unpinEnabled)
|
||||||
downloadButton?.isVisible = (enabled && !deleteEnabled && !isOffline())
|
downloadButton?.isVisible = (enabled && !deleteEnabled && !isOffline())
|
||||||
|
@ -562,8 +564,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
||||||
|
|
||||||
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
|
val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
|
||||||
|
|
||||||
// Hide select button for video lists
|
// Hide select button for video lists and singular selection lists
|
||||||
selectButton!!.isVisible = !allVideos
|
selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection())
|
||||||
|
|
||||||
if (songCount > 0) {
|
if (songCount > 0) {
|
||||||
if (listSize == 0 || songCount < listSize) {
|
if (listSize == 0 || songCount < listSize) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import java.util.Comparator
|
||||||
|
import java.util.SortedSet
|
||||||
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TreeSet that ensures it never grows beyond a max size.
|
||||||
|
* `last()` is removed if the `size()`
|
||||||
|
* get's bigger then `getMaxSize()`
|
||||||
|
*/
|
||||||
|
class BoundedTreeSet<E> : TreeSet<E> {
|
||||||
|
private var maxSize = Int.MAX_VALUE
|
||||||
|
|
||||||
|
constructor(maxSize: Int) : super() {
|
||||||
|
setMaxSize(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(maxSize: Int, c: Collection<E>?) : super(c) {
|
||||||
|
setMaxSize(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(maxSize: Int, c: Comparator<in E>?) : super(c) {
|
||||||
|
setMaxSize(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(maxSize: Int, s: SortedSet<E>?) : super(s) {
|
||||||
|
setMaxSize(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMaxSize(): Int {
|
||||||
|
return maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMaxSize(max: Int) {
|
||||||
|
maxSize = max
|
||||||
|
adjust()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjust() {
|
||||||
|
while (maxSize < size) {
|
||||||
|
remove(last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(element: E): Boolean {
|
||||||
|
val out = super.add(element)
|
||||||
|
adjust()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAll(elements: Collection<E>): Boolean {
|
||||||
|
val out = super.addAll(elements)
|
||||||
|
adjust()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||||
|
a:orientation="vertical"
|
||||||
|
a:layout_width="fill_parent"
|
||||||
|
a:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
a:id="@+id/text"
|
||||||
|
a:text="@string/search.artists"
|
||||||
|
a:layout_width="fill_parent"
|
||||||
|
a:layout_height="wrap_content"
|
||||||
|
a:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
a:textColor="#EFEFEF"
|
||||||
|
a:textStyle="bold"
|
||||||
|
a:background="#ff555555"
|
||||||
|
a:gravity="center_vertical"
|
||||||
|
a:paddingStart="4dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
Loading…
Reference in New Issue