Implement singular selection for Bookmarks
This commit is contained in:
parent
ad793e27a5
commit
5dfb66eec2
|
@ -108,7 +108,6 @@ dependencies {
|
|||
implementation other.rxJava
|
||||
implementation other.rxAndroid
|
||||
implementation other.multiType
|
||||
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
|
||||
|
||||
kapt androidSupport.room
|
||||
|
||||
|
|
|
@ -10,10 +10,18 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import com.drakeet.multitype.MultiTypeAdapter
|
||||
import java.util.TreeSet
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.util.BoundedTreeSet
|
||||
|
||||
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)
|
||||
|
||||
private val diffCallback = GenericDiffCallback<T>()
|
||||
|
@ -26,6 +34,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
|||
return getItem(position).longId
|
||||
}
|
||||
|
||||
|
||||
private fun getItem(position: Int): T {
|
||||
return mDiffer.currentList[position]
|
||||
}
|
||||
|
@ -183,22 +192,33 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
|
|||
list.add(to - 1, fromLocation)
|
||||
}
|
||||
submitList(list)
|
||||
return list as List<T>
|
||||
return list
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Calculates the differences between data sets
|
||||
*/
|
||||
class GenericDiffCallback<T : Identifiable> : DiffUtil.ItemCallback<T>() {
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
fun hasSingleSelection(): Boolean {
|
||||
return selectionType == SelectionType.SINGLE
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
fun hasMultipleSelection(): Boolean {
|
||||
return selectionType == SelectionType.MULTIPLE
|
||||
}
|
||||
|
||||
enum class SelectionType(val size: Int) {
|
||||
SINGLE(1),
|
||||
MULTIPLE(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the differences between data sets
|
||||
*/
|
||||
class GenericDiffCallback<T : Identifiable> : DiffUtil.ItemCallback<T>() {
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
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) {
|
||||
observableChecked.postValue(newStatus)
|
||||
check.isChecked = newStatus
|
||||
//check.isChecked = newStatus
|
||||
}
|
||||
|
||||
override fun isChecked(): Boolean {
|
||||
|
|
|
@ -7,26 +7,36 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setTitle(this, R.string.button_bar_bookmarks)
|
||||
|
||||
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE
|
||||
}
|
||||
|
||||
override fun setupButtons(view: View) {
|
||||
super.setupButtons(view)
|
||||
|
||||
// Why?
|
||||
selectButton?.visibility = View.GONE
|
||||
moreButton?.visibility = View.GONE
|
||||
// Hide select all button
|
||||
//selectButton?.visibility = View.GONE
|
||||
//moreButton?.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
|
||||
|
@ -37,10 +47,6 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||
}
|
||||
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.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||
import org.moire.ultrasonic.adapters.DividerBinder
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
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.VideoPlayer.Companion.playVideo
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
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
|
||||
val arguments = arguments
|
||||
if (arguments != null) {
|
||||
|
@ -415,10 +421,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||
}
|
||||
|
||||
private fun search(query: String, autoplay: Boolean) {
|
||||
// FIXME add error handler
|
||||
// FIXME support autoplay
|
||||
listModel.viewModelScope.launch {
|
||||
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||
listModel.search(query)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -429,7 +435,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||
|
||||
val artists = searchResult.artists
|
||||
if (artists.isNotEmpty()) {
|
||||
// FIXME: addView(albumsHeading)
|
||||
|
||||
list.add(DividerBinder.Divider(R.string.search_artists))
|
||||
list.addAll(artists)
|
||||
if (artists.size > DEFAULT_ARTISTS) {
|
||||
// FIXME
|
||||
|
@ -438,7 +445,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||
}
|
||||
val albums = searchResult.albums
|
||||
if (albums.isNotEmpty()) {
|
||||
// mergeAdapter!!.addView(albumsHeading)
|
||||
list.add(DividerBinder.Divider(R.string.search_albums))
|
||||
list.addAll(albums)
|
||||
// mergeAdapter!!.addAdapter(albumAdapter)
|
||||
// if (albums.size > DEFAULT_ALBUMS) {
|
||||
|
@ -447,8 +454,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||
}
|
||||
val songs = searchResult.songs
|
||||
if (songs.isNotEmpty()) {
|
||||
// mergeAdapter!!.addView(songsHeading)
|
||||
|
||||
list.add(DividerBinder.Divider(R.string.search_albums))
|
||||
list.addAll(songs)
|
||||
// if (songs.size > DEFAULT_SONGS) {
|
||||
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
|||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
|
@ -431,6 +432,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||
val enabled = selection.isNotEmpty()
|
||||
var unpinEnabled = false
|
||||
var deleteEnabled = false
|
||||
val multipleSelection = viewAdapter.hasMultipleSelection()
|
||||
|
||||
var pinnedCount = 0
|
||||
|
||||
|
@ -446,8 +448,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
|
|||
}
|
||||
|
||||
playNowButton?.isVisible = enabled
|
||||
playNextButton?.isVisible = enabled
|
||||
playLastButton?.isVisible = enabled
|
||||
playNextButton?.isVisible = enabled && multipleSelection
|
||||
playLastButton?.isVisible = enabled && multipleSelection
|
||||
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount)
|
||||
unpinButton?.isVisible = (enabled && unpinEnabled)
|
||||
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
|
||||
|
||||
// Hide select button for video lists
|
||||
selectButton!!.isVisible = !allVideos
|
||||
// Hide select button for video lists and singular selection lists
|
||||
selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection())
|
||||
|
||||
if (songCount > 0) {
|
||||
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