diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 6bb8204e..dedf65e2 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 804cb308..3f91a6d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -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 : MultiTypeAdapter() { - internal var selectedSet: TreeSet = 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 = BoundedTreeSet(selectionType.size) internal var selectionRevision: MutableLiveData = MutableLiveData(0) private val diffCallback = GenericDiffCallback() @@ -26,6 +34,7 @@ class BaseAdapter : MultiTypeAdapter() { return getItem(position).longId } + private fun getItem(position: Int): T { return mDiffer.currentList[position] } @@ -183,22 +192,33 @@ class BaseAdapter : MultiTypeAdapter() { list.add(to - 1, fromLocation) } submitList(list) - return list as List + return list } - companion object { - /** - * Calculates the differences between data sets - */ - class GenericDiffCallback : DiffUtil.ItemCallback() { - @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 : DiffUtil.ItemCallback() { + @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 } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt new file mode 100644 index 00000000..3228ee5a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -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() { + + + // 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) + } + + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 000b8614..06eeb313 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -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 { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index e5431d35..5cc077cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -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> { @@ -37,10 +47,6 @@ class BookmarksFragment : TrackCollectionFragment() { } return listModel.currentList } - - override fun enableButtons(selection: List) { - super.enableButtons(selection) - } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt similarity index 97% rename from ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 183f7410..76002c05 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -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(), 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(), 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(), 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(), 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(), 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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index f525fed2..5c9687f4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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() { 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() { } 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() { 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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt new file mode 100644 index 00000000..91bb0b22 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/BoundedTreeSet.kt @@ -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 : TreeSet { + private var maxSize = Int.MAX_VALUE + + constructor(maxSize: Int) : super() { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, c: Collection?) : super(c) { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, c: Comparator?) : super(c) { + setMaxSize(maxSize) + } + + constructor(maxSize: Int, s: SortedSet?) : 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): Boolean { + val out = super.addAll(elements) + adjust() + return out + } +} diff --git a/ultrasonic/src/main/res/layout/row_divider.xml b/ultrasonic/src/main/res/layout/row_divider.xml new file mode 100644 index 00000000..6f931fa2 --- /dev/null +++ b/ultrasonic/src/main/res/layout/row_divider.xml @@ -0,0 +1,20 @@ + + + + + + +