Implement singular selection for Bookmarks

This commit is contained in:
tzugen 2021-11-25 19:44:16 +01:00
parent ad793e27a5
commit 5dfb66eec2
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
9 changed files with 195 additions and 33 deletions

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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>