Various fixes

* Work on folder selector,
* Make current play queue drag&droppable
* Fix album view in offline mode
This commit is contained in:
tzugen 2021-11-28 18:26:44 +01:00
parent 82d90a6aee
commit 2f0ff384d0
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
18 changed files with 150 additions and 90 deletions

View File

@ -24,6 +24,7 @@ import org.moire.ultrasonic.util.BoundedTreeSet
*
* It should be kept generic enough that it can be used a Base for all lists in the app.
*/
@Suppress("unused", "UNUSED_PARAMETER")
class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
// Update the BoundedTreeSet if selection type is changed
@ -195,19 +196,6 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
return selectedSet.contains(longId)
}
fun moveItem(from: Int, to: Int): List<T> {
val list = getCurrentList().toMutableList()
val fromLocation = list[from]
list.removeAt(from)
if (to < from) {
list.add(to + 1, fromLocation)
} else {
list.add(to - 1, fromLocation)
}
submitList(list)
return list
}
fun hasSingleSelection(): Boolean {
return selectionType == SelectionType.SINGLE
}

View File

@ -36,15 +36,22 @@ class FolderSelectorBinder(context: Context) :
}
override fun onBindViewHolder(holder: ViewHolder, item: FolderHeader) {
holder.setData(item.selected, item.folders)
holder.setData(item)
}
class ViewHolder(
view: View,
private val weakContext: WeakReference<Context>
) : RecyclerView.ViewHolder(view) {
private var musicFolders: List<MusicFolder> = mutableListOf()
private var selectedFolderId: String? = null
private var data: FolderHeader? = null
private val selectedFolderId: String?
get() = data?.selected
private val musicFolders: List<MusicFolder>
get() = data?.folders ?: mutableListOf()
private val folderName: TextView = itemView.findViewById(R.id.select_folder_name)
private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header)
@ -53,9 +60,8 @@ class FolderSelectorBinder(context: Context) :
layout.setOnClickListener { onFolderClick() }
}
fun setData(selectedId: String?, folders: List<MusicFolder>) {
selectedFolderId = selectedId
musicFolders = folders
fun setData(item: FolderHeader) {
data = item
if (selectedFolderId != null) {
for ((id, name) in musicFolders) {
if (id == selectedFolderId) {
@ -74,9 +80,11 @@ class FolderSelectorBinder(context: Context) :
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
)
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
menuItem.isChecked = true
}
musicFolders.forEachIndexed { i, musicFolder ->
val (id, name) = musicFolder
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
@ -95,7 +103,8 @@ class FolderSelectorBinder(context: Context) :
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId]
val musicFolderName = selectedFolder?.name
?: weakContext.get()!!.getString(R.string.select_artist_all_folders)
selectedFolderId = selectedFolder?.id
data?.selected = selectedFolder?.id
menuItem.isChecked = true
folderName.text = musicFolderName
@ -111,8 +120,8 @@ class FolderSelectorBinder(context: Context) :
}
data class FolderHeader(
val folders: List<MusicFolder>,
val selected: String?
var folders: List<MusicFolder>,
var selected: String?
) : Identifiable {
override val id: String
get() = "FOLDERSELECTOR"

View File

@ -1,2 +1 @@
package org.moire.ultrasonic.adapters

View File

@ -65,7 +65,7 @@ class TrackViewBinder(
val popup = Utils.createPopupMenu(holder.itemView, contextMenuLayout)
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick?.invoke(menuItem, downloadFile)
onContextMenuClick?.invoke(menuItem, downloadFile)
}
} else {
// Minimize or maximize the Text view (if song title is very long)

View File

@ -7,7 +7,6 @@ import android.widget.Checkable
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData

View File

@ -9,12 +9,15 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.model.AlbumListModel
import org.moire.ultrasonic.util.Constants
@ -84,4 +87,39 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
findNavController().navigate(R.id.trackCollectionFragment, bundle)
}
/**
* What to do when the list has changed
*/
override val defaultObserver: (List<MusicDirectory.Album>) -> Unit = {
emptyView.isVisible = it.isEmpty()
if (showFolderHeader()) {
@Suppress("UNCHECKED_CAST")
val list = it as MutableList<Identifiable>
list.add(0, folderHeader)
} else {
viewAdapter.submitList(it)
}
}
/**
* Get a folder header and update it on changes
*/
private val folderHeader: FolderSelectorBinder.FolderHeader by lazy {
val header = FolderSelectorBinder.FolderHeader(
listModel.musicFolders.value!!,
listModel.activeServer.musicFolderId
)
listModel.musicFolders.observe(
viewLifecycleOwner,
{
header.folders = it
viewAdapter.notifyItemChanged(0)
}
)
header
}
}

View File

@ -9,6 +9,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.model.ArtistListModel
import org.moire.ultrasonic.util.Constants
@ -47,16 +48,29 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
)
}
/**
* There are different targets depending on what list we show.
* If we are showing indexes, we need to go to TrackCollection
* If we are showing artists, we need to go to AlbumList
*/
override fun onItemClick(item: ArtistOrIndex) {
val bundle = Bundle()
// Common arguments
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle)
// Check type
if (item is Index) {
findNavController().navigate(R.id.artistsListToTrackCollection, bundle)
} else {
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
findNavController().navigate(R.id.artistsListToAlbumsList, bundle)
}
}
}

View File

@ -17,7 +17,6 @@ import androidx.lifecycle.LiveData
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.model.GenericListModel
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
@ -56,7 +55,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
viewAdapter.register(
TrackViewBinder(
{ },
{ _,_ -> true },
{ _, _ -> true },
checkable = false,
draggable = false,
context = requireContext(),
@ -78,7 +77,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
}
override fun onItemClick(item: DownloadFile) {
// TODO: Add code to enable manipulation of the download list
// TODO: Add code to enable manipulation of the download list
}
}

View File

@ -6,6 +6,7 @@ import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
@ -24,7 +25,6 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
/**
* Whether to show the folder selector
*/
// FIXME
fun showFolderHeader(): Boolean {
return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Settings.shouldUseId3Tags
@ -55,9 +55,14 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
currentSetting.musicFolderId = it
serverSettingsModel.updateItem(currentSetting)
}
// FIXME: Needed?
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments)
}
viewAdapter.register(
FolderSelectorBinder(view.context)
)
}
companion object {

View File

@ -102,6 +102,14 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
}
}
/**
* What to do when the list has changed
*/
internal open val defaultObserver: ((List<T>) -> Unit) = {
emptyView.isVisible = it.isEmpty()
viewAdapter.submitList(it)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -122,13 +130,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
emptyTextView = view.findViewById(emptyTextId)
// Register an observer to update our UI when the data changes
liveDataItems.observe(
viewLifecycleOwner,
{ newItems ->
emptyView.isVisible = newItems.isEmpty()
viewAdapter.submitList(newItems)
}
)
liveDataItems.observe(viewLifecycleOwner, defaultObserver)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
@ -139,9 +141,6 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
layoutManager = viewManager
adapter = viewAdapter
}
// Configure whether to show the folder header
// viewAdapter.folderHeaderEnabled = showFolderHeader()
}
@Override

View File

@ -859,7 +859,7 @@ class PlayerFragment :
viewAdapter.register(
TrackViewBinder(
onItemClick = listener,
onContextMenuClick = {_,_ -> true},
onContextMenuClick = { _, _ -> true },
checkable = false,
draggable = true,
context = requireContext(),
@ -880,10 +880,9 @@ class PlayerFragment :
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
// FIXME:
// Needs to be changed in the playlist as well...
// Move it in the data set
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
// Move it in the data set
mediaPlayerController.moveItemInPlaylist(from, to)
viewAdapter.submitList(mediaPlayerController.playList)
return true
}

View File

@ -370,7 +370,14 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
@Suppress("LongMethod")
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
val isArtist = (item is Artist)
val found = EntryListFragment.handleContextMenu(menuItem, item, isArtist, downloadHandler, this)
val found = EntryListFragment.handleContextMenu(
menuItem,
item,
isArtist,
downloadHandler,
this
)
if (found || item !is DownloadFile) return true

View File

@ -46,24 +46,24 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* FIXME: Offset when navigating to?
*/
@Suppress("TooManyFunctions")
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
private var albumButtons: View? = null
internal var selectButton: ImageView? = null
internal var playNowButton: ImageView? = null
internal var playNextButton: ImageView? = null
internal var playLastButton: ImageView? = null
private var playNextButton: ImageView? = null
private var playLastButton: ImageView? = null
internal var pinButton: ImageView? = null
internal var unpinButton: ImageView? = null
internal var downloadButton: ImageView? = null
internal var deleteButton: ImageView? = null
internal var moreButton: ImageView? = null
private var unpinButton: ImageView? = null
private var downloadButton: ImageView? = null
private var deleteButton: ImageView? = null
private var moreButton: ImageView? = null
private var playAllButtonVisible = false
private var shareButtonVisible = false
private var playAllButton: MenuItem? = null

View File

@ -6,6 +6,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers
@ -16,14 +17,15 @@ import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Settings
/**
* An abstract Model, which can be extended to retrieve a list of items from the API
*/
* An abstract Model, which can be extended to retrieve a list of items from the API
*/
open class GenericListModel(application: Application) :
AndroidViewModel(application), KoinComponent {
@ -38,6 +40,8 @@ open class GenericListModel(application: Application) :
var currentListIsSortable = true
var showHeader = true
val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
@Suppress("UNUSED_PARAMETER")
open fun showSelectFolderHeader(args: Bundle?): Boolean {
return true
@ -105,8 +109,11 @@ open class GenericListModel(application: Application) :
args: Bundle
) {
// Update the list of available folders if enabled
// FIXME && refresh ?
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
// FIXME
musicFolders.postValue(
musicService.getMusicFolders(refresh)
)
}
}

View File

@ -369,6 +369,20 @@ class Downloader(
checkDownloads()
}
fun moveItemInPlaylist(oldPos: Int, newPos: Int) {
val item = playlist[oldPos]
playlist.remove(item)
if (newPos < oldPos) {
playlist.add(newPos + 1, item)
} else {
playlist.add(newPos - 1, item)
}
playlistUpdateRevision++
checkDownloads()
}
@Synchronized
fun clearIncomplete() {
val iterator = playlist.iterator()

View File

@ -250,6 +250,11 @@ class MediaPlayerController(
mediaPlayerService?.setNextPlaying()
}
@Synchronized
fun moveItemInPlaylist(oldPos: Int, newPos: Int) {
downloader.moveItemInPlaylist(oldPos, newPos)
}
@set:Synchronized
var repeatMode: RepeatMode
get() = Settings.repeatMode

View File

@ -1,28 +0,0 @@
package org.moire.ultrasonic.util
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.adapters.BaseAdapter
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
// FIXME: Move it in the data set
(recyclerView.adapter as BaseAdapter<*>).moveItem(from, to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}

View File

@ -25,15 +25,21 @@
android:name="org.moire.ultrasonic.fragment.ArtistListFragment"
android:label="@string/music_library.label" >
<action
android:id="@+id/selectArtistToSelectAlbum"
android:id="@+id/artistsListToAlbumsList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/artistsListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/artistListFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
<action
android:id="@+id/selectArtistToSelectAlbum"
android:id="@+id/artistsListToAlbumsList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/artistsListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/trackCollectionFragment"