Make SearchResults expandable,

finish music folder support,
change Service interface of AlbumList to return listOf(Album)
This commit is contained in:
tzugen 2021-11-30 00:46:48 +01:00
parent aa33d7c882
commit bdac092eff
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
28 changed files with 319 additions and 341 deletions

View File

@ -51,6 +51,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract var starred: Boolean
abstract var path: String?
abstract var closeness: Int
abstract var isVideo: Boolean
}
// TODO: Rename to Track
@ -77,7 +78,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
override var duration: Int? = null,
var bitRate: Int? = null,
override var path: String? = null,
var isVideo: Boolean = false,
override var isVideo: Boolean = false,
override var starred: Boolean = false,
override var discNumber: Int? = null,
var type: String? = null,
@ -133,5 +134,6 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
override var closeness: Int = 0,
) : Child() {
override var isDirectory = true
override var isVideo = false
}
}

View File

@ -3,7 +3,7 @@ package org.moire.ultrasonic.api.subsonic.response
import com.fasterxml.jackson.annotation.JsonProperty
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.Album
class GetAlbumListResponse(
status: Status,
@ -12,10 +12,10 @@ class GetAlbumListResponse(
) : SubsonicResponse(status, version, error) {
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
val albumList: List<MusicDirectoryChild>
val albumList: List<Album>
get() = albumWrapper.albumList
}
private class AlbumWrapper(
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList()
@JsonProperty("album") val albumList: List<Album> = emptyList()
)

View File

@ -1,72 +0,0 @@
package org.moire.ultrasonic.util;
import org.moire.ultrasonic.domain.MusicDirectory;
import java.io.Serializable;
import java.util.Comparator;
public class EntryByDiscAndTrackComparator implements Comparator<MusicDirectory.Entry>, Serializable
{
private static final long serialVersionUID = 5540441864560835223L;
@Override
public int compare(MusicDirectory.Entry x, MusicDirectory.Entry y)
{
Integer discX = x.getDiscNumber();
Integer discY = y.getDiscNumber();
Integer trackX = x.getTrack();
Integer trackY = y.getTrack();
String albumX = x.getAlbum();
String albumY = y.getAlbum();
String pathX = x.getPath();
String pathY = y.getPath();
int albumComparison = compare(albumX, albumY);
if (albumComparison != 0)
{
return albumComparison;
}
int discComparison = compare(discX == null ? 0 : discX, discY == null ? 0 : discY);
if (discComparison != 0)
{
return discComparison;
}
int trackComparison = compare(trackX == null ? 0 : trackX, trackY == null ? 0 : trackY);
if (trackComparison != 0)
{
return trackComparison;
}
return compare(pathX == null ? "" : pathX, pathY == null ? "" : pathY);
}
private static int compare(long a, long b)
{
return Long.compare(a, b);
}
private static int compare(String a, String b)
{
if (a == null && b == null)
{
return 0;
}
if (a == null)
{
return -1;
}
if (b == null)
{
return 1;
}
return a.compareTo(b);
}
}

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.util
package org.moire.ultrasonic.adapters
import java.util.HashSet
import org.moire.ultrasonic.domain.Identifiable
@ -7,7 +7,7 @@ import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName
import org.moire.ultrasonic.util.Util.getGrandparent
class AlbumHeader(
var entries: List<MusicDirectory.Entry>,
var entries: List<MusicDirectory.Child>,
var name: String?
) : Identifiable {
var isAllVideo: Boolean
@ -35,7 +35,7 @@ class AlbumHeader(
val years: Set<Int>
get() = _years
private fun processGrandParents(entry: MusicDirectory.Entry) {
private fun processGrandParents(entry: MusicDirectory.Child) {
val grandParent = getGrandparent(entry.path)
if (grandParent != null) {
_grandParents.add(grandParent)
@ -43,7 +43,7 @@ class AlbumHeader(
}
@Suppress("NestedBlockDepth")
private fun processEntries(list: List<MusicDirectory.Entry>) {
private fun processEntries(list: List<MusicDirectory.Child>) {
entries = list
childCount = entries.size
for (entry in entries) {

View File

@ -16,6 +16,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
// Set our layout files
val layout = R.layout.list_item_divider
val more_button = R.layout.list_item_more_button
override fun onBindViewHolder(holder: ViewHolder, item: Divider) {
// Set text

View File

@ -15,7 +15,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.Util
/**

View File

@ -0,0 +1,50 @@
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 MoreButtonBinder : ItemViewBinder<MoreButtonBinder.MoreButton, RecyclerView.ViewHolder>() {
// Set our layout files
val layout = R.layout.list_item_more_button
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: MoreButton) {
holder.itemView.setOnClickListener {
item.onClick()
}
}
override fun onCreateViewHolder(
inflater: LayoutInflater,
parent: ViewGroup
): RecyclerView.ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}
// ViewHolder class
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
// Class to store our data into
data class MoreButton(
val stringId: Int,
val onClick: (() -> Unit)
): 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

@ -7,7 +7,8 @@ import org.moire.ultrasonic.api.subsonic.models.Album
fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album(
id = this@toDomainEntity.id,
title = this@toDomainEntity.name,
title = this@toDomainEntity.title,
album = this@toDomainEntity.album,
coverArt = this@toDomainEntity.coverArt,
artist = this@toDomainEntity.artist,
artistId = this@toDomainEntity.artistId,

View File

@ -24,7 +24,6 @@ import org.moire.ultrasonic.util.Constants
/**
* Displays a list of Albums from the media library
* FIXME: Add music folder support
*/
class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
@ -41,10 +40,10 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Album>> {
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Album>> {
if (args == null) throw IllegalArgumentException("Required arguments are missing")
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH)
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) || refresh
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND)
return listModel.getAlbumList(refresh or append, refreshListView!!, args)
@ -87,39 +86,4 @@ 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

@ -2,19 +2,25 @@ 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 org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.model.ArtistListModel
import org.moire.ultrasonic.util.Constants
/**
* Displays the list of Artists from the media library
*
* FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD
*/
class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
@ -31,8 +37,8 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<ArtistOrIndex>> {
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<ArtistOrIndex>> {
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh
return listModel.getItems(refresh, refreshListView!!)
}

View File

@ -24,7 +24,6 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
* audio books etc.
*
* Therefore this fragment allows only for singular selection and playback.
*
*/
class BookmarksFragment : TrackCollectionFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -35,7 +34,7 @@ class BookmarksFragment : TrackCollectionFragment() {
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE
}
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Child>> {
listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true
listModel.getBookmarks()

View File

@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<DownloadFile>> {
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<DownloadFile>> {
return listModel.getList()
}

View File

@ -3,11 +3,13 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
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.ArtistOrIndex
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.RxBus
@ -48,15 +50,12 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// FIXME: What to do when the user has modified the folder filter
RxBus.musicFolderChangedEventObservable.subscribe {
if (!listModel.isOffline()) {
val currentSetting = listModel.activeServer
currentSetting.musicFolderId = it
serverSettingsModel.updateItem(currentSetting)
}
// FIXME: Needed?
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments)
}
@ -65,6 +64,41 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
)
}
/**
* What to do when the list has changed
*/
override val defaultObserver: (List<T>) -> Unit = {
emptyView.isVisible = it.isEmpty()
if (showFolderHeader()) {
val list = mutableListOf<Identifiable>(folderHeader)
list.addAll(it)
viewAdapter.submitList(list)
} 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
}
companion object {
@Suppress("LongMethod")
internal fun handleContextMenu(

View File

@ -72,7 +72,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
open fun getLiveData(args: Bundle? = null): LiveData<List<T>> {
open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData<List<T>> {
return MutableLiveData()
}
@ -123,7 +123,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
}
// Populate the LiveData. This starts an API request in most cases
liveDataItems = getLiveData(arguments)
liveDataItems = getLiveData(arguments, true)
// Link view to display text if the list is empty
emptyView = view.findViewById(emptyViewId)

View File

@ -22,6 +22,8 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.DividerBinder
import org.moire.ultrasonic.adapters.MoreButtonBinder
import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Identifiable
@ -44,18 +46,10 @@ import timber.log.Timber
/**
* Initiates a search on the media library and displays the results
*
* FIXME: Handle context click on song
* FIXME: Artist click, display
*/
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private var moreArtistsButton: View? = null
private var moreAlbumsButton: View? = null
private var moreSongsButton: View? = null
private var searchResult: SearchResult? = null
private var artistAdapter: ArtistAdapter? = null
private var moreArtistsAdapter: ListAdapter? = null
private var moreAlbumsAdapter: ListAdapter? = null
private var moreSongsAdapter: ListAdapter? = null
private var searchRefresh: SwipeRefreshLayout? = null
private val mediaPlayerController: MediaPlayerController by inject()
@ -75,40 +69,20 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
setTitle(this, R.string.search_title)
setHasOptionsMenu(true)
val buttons = LayoutInflater.from(context).inflate(
R.layout.search_buttons,
listView, false
)
if (buttons != null) {
moreArtistsButton = buttons.findViewById(R.id.search_more_artists)
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums)
moreSongsButton = buttons.findViewById(R.id.search_more_songs)
}
listModel.searchResult.observe(
viewLifecycleOwner,
{
if (it != null) populateList(it)
if (it != null) {
// Shorten the display initially
searchResult = it
populateList(listModel.trimResultLength(it))
}
}
)
searchRefresh = view.findViewById(R.id.swipe_refresh_view)
searchRefresh!!.isEnabled = false
// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long ->
// if (view1 === moreArtistsButton) {
// expandArtists()
// } else if (view1 === moreAlbumsButton) {
// expandAlbums()
// } else if (view1 === moreSongsButton) {
// expandSongs()
// } else {
// val item = parent.getItemAtPosition(position)
//
// }
// })
registerForContextMenu(listView!!)
// Register our data binders
@ -147,6 +121,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
DividerBinder()
)
viewAdapter.register(
MoreButtonBinder()
)
// Fragment was started with a query (e.g. from voice search), try to execute search right away
val arguments = arguments
if (arguments != null) {
@ -229,45 +207,44 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
}
private fun search(query: String, autoplay: Boolean) {
// FIXME support autoplay
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
refreshListView?.isRefreshing = true
listModel.search(query)
refreshListView?.isRefreshing = false
}.invokeOnCompletion {
if (it == null && autoplay) {
autoplay()
}
}
}
private fun populateList(result: SearchResult) {
val searchResult = listModel.trimResultLength(result)
val list = mutableListOf<Identifiable>()
val artists = searchResult.artists
val artists = result.artists
if (artists.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_artists))
list.addAll(artists)
if (artists.size > DEFAULT_ARTISTS) {
// FIXME
// list.add((moreArtistsButton, true)
if (searchResult!!.artists.size > artists.size) {
list.add(MoreButton(0, ::expandArtists))
}
}
val albums = searchResult.albums
val albums = result.albums
if (albums.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_albums))
list.addAll(albums)
// mergeAdapter!!.addAdapter(albumAdapter)
// if (albums.size > DEFAULT_ALBUMS) {
// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true)
// }
if (searchResult!!.albums.size > albums.size) {
list.add(MoreButton(1, ::expandAlbums))
}
}
val songs = searchResult.songs
val songs = result.songs
if (songs.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_songs))
list.addAll(songs)
// if (songs.size > DEFAULT_SONGS) {
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
// }
if (searchResult!!.songs.size > songs.size) {
list.add(MoreButton(2, ::expandSongs))
}
}
// Show/hide the empty text view
@ -276,35 +253,17 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
viewAdapter.submitList(list)
}
// private fun expandArtists() {
// artistAdapter!!.clear()
// for (artist in searchResult!!.artists) {
// artistAdapter!!.add(artist)
// }
// artistAdapter!!.notifyDataSetChanged()
// mergeAdapter!!.removeAdapter(moreArtistsAdapter)
// mergeAdapter!!.notifyDataSetChanged()
// }
//
// private fun expandAlbums() {
// albumAdapter!!.clear()
// for (album in searchResult!!.albums) {
// albumAdapter!!.add(album)
// }
// albumAdapter!!.notifyDataSetChanged()
// mergeAdapter!!.removeAdapter(moreAlbumsAdapter)
// mergeAdapter!!.notifyDataSetChanged()
// }
//
// private fun expandSongs() {
// songAdapter!!.clear()
// for (song in searchResult!!.songs) {
// songAdapter!!.add(song)
// }
// songAdapter!!.notifyDataSetChanged()
// mergeAdapter!!.removeAdapter(moreSongsAdapter)
// mergeAdapter!!.notifyDataSetChanged()
// }
private fun expandArtists() {
populateList(listModel.trimResultLength(searchResult!!, maxArtists = Int.MAX_VALUE))
}
private fun expandAlbums() {
populateList(listModel.trimResultLength(searchResult!!, maxAlbums = Int.MAX_VALUE))
}
private fun expandSongs() {
populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE))
}
private fun onArtistSelected(artist: Artist) {
val bundle = Bundle()
@ -343,12 +302,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
}
}
companion object {
var DEFAULT_ARTISTS = Settings.defaultArtists
var DEFAULT_ALBUMS = Settings.defaultAlbums
var DEFAULT_SONGS = Settings.defaultSongs
}
override fun onItemClick(item: Identifiable) {
when (item) {
is Artist -> {
@ -464,4 +417,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
return true
}
companion object {
var DEFAULT_ARTISTS = Settings.defaultArtists
var DEFAULT_ALBUMS = Settings.defaultAlbums
var DEFAULT_SONGS = Settings.defaultSongs
}
}

View File

@ -28,6 +28,8 @@ 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.AlbumHeader
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
@ -39,7 +41,6 @@ import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants
@ -48,11 +49,16 @@ import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
/**
*
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* FIXME: Mixed lists are not handled correctly
*
* In most cases the data should be just a list of Entries, but there are some cases
* where the list can contain Albums as well. This happens especially when having ID3 tags disabled,
* or using Offline mode, both in which Indexes instead of Artists are being used.
*
*/
@Suppress("TooManyFunctions")
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private var albumButtons: View? = null
internal var selectButton: ImageView? = null
@ -128,6 +134,15 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
)
)
viewAdapter.register(
AlbumRowBinder(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader(),
context = requireContext()
)
)
enableButtons()
// Update the buttons when the selection has changed
@ -447,9 +462,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
}
}
override val defaultObserver: (List<MusicDirectory.Entry>) -> Unit = {
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
val entryList: MutableList<MusicDirectory.Entry> = it.toMutableList()
val entryList: MutableList<MusicDirectory.Child> = it.toMutableList()
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(entryList, EntryByDiscAndTrackComparator())
@ -470,7 +485,7 @@ 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 and singular selection lists
selectButton!!.isVisible = (!allVideos && viewAdapter.hasMultipleSelection())
selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0
if (songCount > 0) {
if (listSize == 0 || songCount < listSize) {
@ -550,12 +565,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
}
@Suppress("LongMethod")
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<MusicDirectory.Child>> {
if (args == null) return listModel.currentList
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME)
val parentId = args.getString(Constants.INTENT_EXTRA_NAME_PARENT_ID)
val playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
val podcastChannelId = args.getString(
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
@ -574,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
val albumListOffset = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
)
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true) || refresh
listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true
@ -621,7 +635,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
@Suppress("LongMethod")
override fun onContextMenuItemSelected(
menuItem: MenuItem,
item: MusicDirectory.Entry
item: MusicDirectory.Child
): Boolean {
val entryId = item.id
@ -673,13 +687,12 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
playAll()
}
R.id.menu_item_share -> {
val entries: MutableList<MusicDirectory.Entry?> = ArrayList(1)
entries.add(item)
shareHandler.createShare(
this, entries, refreshListView,
cancellationToken!!
)
return true
if (item is MusicDirectory.Entry) {
shareHandler.createShare(
this, listOf(item), refreshListView,
cancellationToken!!
)
}
}
else -> {
return super.onContextItemSelected(menuItem)
@ -688,7 +701,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
return true
}
override fun onItemClick(item: MusicDirectory.Entry) {
override fun onItemClick(item: MusicDirectory.Child) {
when {
item.isDirectory -> {
val bundle = Bundle()
@ -701,7 +714,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
bundle
)
}
item.isVideo -> {
item is MusicDirectory.Entry && item.isVideo -> {
VideoPlayer.playVideo(requireContext(), item)
}
else -> {

View File

@ -33,7 +33,12 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
return list
}
fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) {
private fun getAlbumsOfArtist(
musicService: MusicService,
refresh: Boolean,
id: String,
name: String?
) {
list.postValue(musicService.getArtist(id, name, refresh))
}
@ -51,7 +56,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false)
val musicDirectory: MusicDirectory
val musicDirectory: List<MusicDirectory.Album>
val musicFolderId = if (showSelectFolderHeader(args)) {
activeServerProvider.getActiveServer().musicFolderId
} else {
@ -72,10 +77,11 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
}
if (useId3Tags) {
musicDirectory = musicService.getAlbumList2(
albumListType, size,
offset, musicFolderId
)
musicDirectory =
musicService.getAlbumList2(
albumListType, size,
offset, musicFolderId
)
} else {
musicDirectory = musicService.getAlbumList(
albumListType, size,
@ -85,15 +91,13 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
currentListIsSortable = isCollectionSortable(albumListType)
// TODO: Change signature of musicService.getAlbumList to return a List
@Suppress("UNCHECKED_CAST")
if (append && list.value != null) {
val list = ArrayList<MusicDirectory.Child>()
list.addAll(this.list.value!!)
list.addAll(musicDirectory.getChildren())
this.list.postValue(list as List<MusicDirectory.Album>)
val newList = ArrayList<MusicDirectory.Album>()
newList.addAll(list.value!!)
newList.addAll(musicDirectory)
this.list.postValue(newList)
} else {
list.postValue(musicDirectory.getChildren() as List<MusicDirectory.Album>)
list.postValue(musicDirectory)
}
loadedUntil = offset

View File

@ -16,7 +16,6 @@ import org.koin.core.component.KoinComponent
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
@ -44,7 +43,7 @@ open class GenericListModel(application: Application) :
@Suppress("UNUSED_PARAMETER")
open fun showSelectFolderHeader(args: Bundle?): Boolean {
return true
return false
}
/**
@ -109,20 +108,11 @@ open class GenericListModel(application: Application) :
args: Bundle
) {
// Update the list of available folders if enabled
// FIXME && refresh ?
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
@Suppress("ComplexCondition")
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) {
musicFolders.postValue(
musicService.getMusicFolders(refresh)
)
}
}
/**
* Some shared helper functions
*/
// Returns true if the directory contains only folders
internal fun hasOnlyFolders(musicDirectory: MusicDirectory) =
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
}

View File

@ -40,11 +40,16 @@ class SearchListModel(application: Application) : GenericListModel(application)
}
}
fun trimResultLength(result: SearchResult): SearchResult {
fun trimResultLength(
result: SearchResult,
maxArtists: Int = SearchFragment.DEFAULT_ARTISTS,
maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS,
maxSongs: Int = SearchFragment.DEFAULT_SONGS
): SearchResult {
return SearchResult(
artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS),
albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS),
songs = result.songs.take(SearchFragment.DEFAULT_SONGS)
artists = result.artists.take(maxArtists),
albums = result.albums.take(maxAlbums),
songs = result.songs.take(maxSongs)
)
}
}

View File

@ -21,7 +21,7 @@ import org.moire.ultrasonic.util.Util
*/
class TrackCollectionModel(application: Application) : GenericListModel(application) {
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
val currentList: MutableLiveData<List<MusicDirectory.Child>> = MutableLiveData()
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
/*
@ -72,7 +72,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
} else {
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
}
updateList(musicDirectory)
}
}
@ -83,7 +83,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val videos = service.getVideos(refresh)
if (videos != null) {
updateList(videos)
}
@ -97,7 +97,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val musicDirectory = service.getRandomSongs(size)
currentListIsSortable = false
updateList(musicDirectory)
}
}
@ -117,7 +117,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
if (musicDirectory != null) {
updateList(musicDirectory)
}
@ -140,7 +140,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
break
}
}
updateList(musicDirectory)
}
}
@ -149,12 +149,12 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks())
updateList(musicDirectory)
}
}
private fun updateList(root: MusicDirectory) {
currentList.postValue(root.getTracks())
currentList.postValue(root.getChildren())
}
}

View File

@ -575,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
}
}
albums?.getChildren()?.map { album ->
albums?.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)

View File

@ -249,7 +249,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
): List<MusicDirectory.Album> {
return musicService.getAlbumList(type, size, offset, musicFolderId)
}
@ -259,7 +259,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
): List<MusicDirectory.Album> {
return musicService.getAlbumList2(type, size, offset, musicFolderId)
}

View File

@ -90,7 +90,12 @@ interface MusicService {
fun scrobble(id: String, submission: Boolean)
@Throws(Exception::class)
fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory
fun getAlbumList(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): List<MusicDirectory.Album>
@Throws(Exception::class)
fun getAlbumList2(
@ -98,7 +103,7 @@ interface MusicService {
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory
): List<MusicDirectory.Album>
@Throws(Exception::class)
fun getRandomSongs(size: Int): MusicDirectory

View File

@ -296,10 +296,20 @@ class OfflineMusicService : MusicService, KoinComponent {
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
): List<MusicDirectory.Album> {
throw OfflineException("Album lists not available in offline mode")
}
@Throws(OfflineException::class)
override fun getAlbumList2(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): List<MusicDirectory.Album> {
throw OfflineException("getAlbumList2 isn't available in offline mode")
}
@Throws(Exception::class)
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
@ -389,16 +399,6 @@ class OfflineMusicService : MusicService, KoinComponent {
throw OfflineException("Music folders not available in offline mode")
}
@Throws(OfflineException::class)
override fun getAlbumList2(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
throw OfflineException("getAlbumList2 isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getVideoUrl(id: String): String? {
throw OfflineException("getVideoUrl isn't available in offline mode")
@ -512,7 +512,6 @@ class OfflineMusicService : MusicService, KoinComponent {
return album
}
/*
* Extracts some basic data from a File object and applies it to an Album or Entry
*/
@ -531,7 +530,6 @@ class OfflineMusicService : MusicService, KoinComponent {
}
}
/*
* More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of
* a given track file.
@ -559,7 +557,7 @@ class OfflineMusicService : MusicService, KoinComponent {
artist = meta.artist ?: file.parentFile!!.parentFile!!.name
album = meta.album ?: file.parentFile!!.name
title = meta.title?: title
title = meta.title ?: title
isVideo = meta.hasVideo != null
track = parseSlashedNumber(meta.track)
discNumber = parseSlashedNumber(meta.disc)
@ -660,7 +658,6 @@ class OfflineMusicService : MusicService, KoinComponent {
return closeness
}
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
for (file in FileUtil.listMediaFiles(parent)) {
if (file.isFile) {

View File

@ -350,7 +350,7 @@ open class RESTMusicService(
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
): List<MusicDirectory.Album> {
val response = API.getAlbumList(
fromName(type),
size,
@ -361,11 +361,8 @@ open class RESTMusicService(
musicFolderId
).execute().throwOnFailure()
val childList = response.body()!!.albumList.toDomainEntityList()
val result = MusicDirectory()
result.addAll(childList)
return result
return response.body()!!.albumList.toDomainEntityList()
}
@Throws(Exception::class)
@ -374,7 +371,7 @@ open class RESTMusicService(
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
): List<MusicDirectory.Album> {
val response = API.getAlbumList2(
fromName(type),
size,
@ -385,10 +382,7 @@ open class RESTMusicService(
musicFolderId
).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.albumList.toDomainEntityList())
return result
return response.body()!!.albumList.toDomainEntityList()
}
@Throws(Exception::class)

View File

@ -0,0 +1,50 @@
package org.moire.ultrasonic.util
import java.util.Comparator
import org.moire.ultrasonic.domain.MusicDirectory
class EntryByDiscAndTrackComparator : Comparator<MusicDirectory.Child> {
override fun compare(x: MusicDirectory.Child, y: MusicDirectory.Child): Int {
val discX = x.discNumber
val discY = y.discNumber
val trackX = if (x is MusicDirectory.Entry) x.track else null
val trackY = if (y is MusicDirectory.Entry) y.track else null
val albumX = x.album
val albumY = y.album
val pathX = x.path
val pathY = y.path
val albumComparison = compare(albumX, albumY)
if (albumComparison != 0) {
return albumComparison
}
val discComparison = compare(discX ?: 0, discY ?: 0)
if (discComparison != 0) {
return discComparison
}
val trackComparison = compare(trackX ?: 0, trackY ?: 0)
return if (trackComparison != 0) {
trackComparison
} else compare(
pathX ?: "",
pathY ?: ""
)
}
companion object {
private fun compare(a: Int, b: Int): Int {
return a.compareTo(b)
}
private fun compare(a: String?, b: String?): Int {
if (a == null && b == null) {
return 0
}
if (a == null) {
return -1
}
return if (b == null) {
1
} else a.compareTo(b)
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/search_more"
android:text="@string/search.more"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="8dp"/>
</LinearLayout>

View File

@ -1,39 +0,0 @@
<?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/search_more_artists"
a:text="@string/search.more"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:gravity="center"
a:paddingTop="8dp"
a:paddingBottom="8dp"/>
<TextView
a:id="@+id/search_more_albums"
a:text="@string/search.more"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:gravity="center"
a:paddingTop="8dp"
a:paddingBottom="8dp"/>
<TextView
a:id="@+id/search_more_songs"
a:text="@string/search.more"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:gravity="center"
a:paddingTop="8dp"
a:paddingBottom="8dp"/>
</LinearLayout>