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 starred: Boolean
abstract var path: String? abstract var path: String?
abstract var closeness: Int abstract var closeness: Int
abstract var isVideo: Boolean
} }
// TODO: Rename to Track // TODO: Rename to Track
@ -77,7 +78,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
override var duration: Int? = null, override var duration: Int? = null,
var bitRate: Int? = null, var bitRate: Int? = null,
override var path: String? = null, override var path: String? = null,
var isVideo: Boolean = false, override var isVideo: Boolean = false,
override var starred: Boolean = false, override var starred: Boolean = false,
override var discNumber: Int? = null, override var discNumber: Int? = null,
var type: String? = null, var type: String? = null,
@ -133,5 +134,6 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
override var closeness: Int = 0, override var closeness: Int = 0,
) : Child() { ) : Child() {
override var isDirectory = true 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 com.fasterxml.jackson.annotation.JsonProperty
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError 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( class GetAlbumListResponse(
status: Status, status: Status,
@ -12,10 +12,10 @@ class GetAlbumListResponse(
) : SubsonicResponse(status, version, error) { ) : SubsonicResponse(status, version, error) {
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper() @JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
val albumList: List<MusicDirectoryChild> val albumList: List<Album>
get() = albumWrapper.albumList get() = albumWrapper.albumList
} }
private class AlbumWrapper( 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 java.util.HashSet
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
@ -7,7 +7,7 @@ import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName
import org.moire.ultrasonic.util.Util.getGrandparent import org.moire.ultrasonic.util.Util.getGrandparent
class AlbumHeader( class AlbumHeader(
var entries: List<MusicDirectory.Entry>, var entries: List<MusicDirectory.Child>,
var name: String? var name: String?
) : Identifiable { ) : Identifiable {
var isAllVideo: Boolean var isAllVideo: Boolean
@ -35,7 +35,7 @@ class AlbumHeader(
val years: Set<Int> val years: Set<Int>
get() = _years get() = _years
private fun processGrandParents(entry: MusicDirectory.Entry) { private fun processGrandParents(entry: MusicDirectory.Child) {
val grandParent = getGrandparent(entry.path) val grandParent = getGrandparent(entry.path)
if (grandParent != null) { if (grandParent != null) {
_grandParents.add(grandParent) _grandParents.add(grandParent)
@ -43,7 +43,7 @@ class AlbumHeader(
} }
@Suppress("NestedBlockDepth") @Suppress("NestedBlockDepth")
private fun processEntries(list: List<MusicDirectory.Entry>) { private fun processEntries(list: List<MusicDirectory.Child>) {
entries = list entries = list
childCount = entries.size childCount = entries.size
for (entry in entries) { for (entry in entries) {

View File

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

View File

@ -15,7 +15,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.Util 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( fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album(
id = this@toDomainEntity.id, id = this@toDomainEntity.id,
title = this@toDomainEntity.name, title = this@toDomainEntity.title,
album = this@toDomainEntity.album,
coverArt = this@toDomainEntity.coverArt, coverArt = this@toDomainEntity.coverArt,
artist = this@toDomainEntity.artist, artist = this@toDomainEntity.artist,
artistId = this@toDomainEntity.artistId, 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 * Displays a list of Albums from the media library
* FIXME: Add music folder support
*/ */
class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() { 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 * 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") 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) val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND)
return listModel.getAlbumList(refresh or append, refreshListView!!, args) 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) bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
findNavController().navigate(R.id.trackCollectionFragment, bundle) 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.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.model.ArtistListModel import org.moire.ultrasonic.model.ArtistListModel
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
/** /**
* Displays the list of Artists from the media library * Displays the list of Artists from the media library
*
* FIXME: FOLDER HEADER NOT POPULATED ON FIST LOAD
*/ */
class ArtistListFragment : EntryListFragment<ArtistOrIndex>() { 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 * The central function to pass a query to the model and return a LiveData object
*/ */
override fun getLiveData(args: Bundle?): LiveData<List<ArtistOrIndex>> { override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<ArtistOrIndex>> {
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false || refresh
return listModel.getItems(refresh, refreshListView!!) return listModel.getItems(refresh, refreshListView!!)
} }

View File

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

View File

@ -3,11 +3,13 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
@ -48,15 +50,12 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// FIXME: What to do when the user has modified the folder filter
RxBus.musicFolderChangedEventObservable.subscribe { RxBus.musicFolderChangedEventObservable.subscribe {
if (!listModel.isOffline()) { if (!listModel.isOffline()) {
val currentSetting = listModel.activeServer val currentSetting = listModel.activeServer
currentSetting.musicFolderId = it currentSetting.musicFolderId = it
serverSettingsModel.updateItem(currentSetting) serverSettingsModel.updateItem(currentSetting)
} }
// FIXME: Needed?
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments) 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 { companion object {
@Suppress("LongMethod") @Suppress("LongMethod")
internal fun handleContextMenu( 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 * 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() return MutableLiveData()
} }
@ -123,7 +123,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
} }
// Populate the LiveData. This starts an API request in most cases // 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 // Link view to display text if the list is empty
emptyView = view.findViewById(emptyViewId) 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.AlbumRowBinder
import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.DividerBinder 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.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Identifiable 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 * Initiates a search on the media library and displays the results
* * FIXME: Artist click, display
* FIXME: Handle context click on song
*/ */
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent { 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 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 var searchRefresh: SwipeRefreshLayout? = null
private val mediaPlayerController: MediaPlayerController by inject() private val mediaPlayerController: MediaPlayerController by inject()
@ -75,40 +69,20 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
setTitle(this, R.string.search_title) setTitle(this, R.string.search_title)
setHasOptionsMenu(true) 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( listModel.searchResult.observe(
viewLifecycleOwner, 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 = view.findViewById(R.id.swipe_refresh_view)
searchRefresh!!.isEnabled = false 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!!) registerForContextMenu(listView!!)
// Register our data binders // Register our data binders
@ -147,6 +121,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
DividerBinder() DividerBinder()
) )
viewAdapter.register(
MoreButtonBinder()
)
// Fragment was started with a query (e.g. from voice search), try to execute search right away // Fragment was started with a query (e.g. from voice search), try to execute search right away
val arguments = arguments val arguments = arguments
if (arguments != null) { if (arguments != null) {
@ -229,45 +207,44 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
} }
private fun search(query: String, autoplay: Boolean) { private fun search(query: String, autoplay: Boolean) {
// FIXME support autoplay
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
refreshListView?.isRefreshing = true refreshListView?.isRefreshing = true
listModel.search(query) listModel.search(query)
refreshListView?.isRefreshing = false refreshListView?.isRefreshing = false
}.invokeOnCompletion {
if (it == null && autoplay) {
autoplay()
}
} }
} }
private fun populateList(result: SearchResult) { private fun populateList(result: SearchResult) {
val searchResult = listModel.trimResultLength(result)
val list = mutableListOf<Identifiable>() val list = mutableListOf<Identifiable>()
val artists = searchResult.artists val artists = result.artists
if (artists.isNotEmpty()) { if (artists.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_artists)) list.add(DividerBinder.Divider(R.string.search_artists))
list.addAll(artists) list.addAll(artists)
if (artists.size > DEFAULT_ARTISTS) { if (searchResult!!.artists.size > artists.size) {
// FIXME list.add(MoreButton(0, ::expandArtists))
// list.add((moreArtistsButton, true)
} }
} }
val albums = searchResult.albums val albums = result.albums
if (albums.isNotEmpty()) { if (albums.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_albums)) list.add(DividerBinder.Divider(R.string.search_albums))
list.addAll(albums) list.addAll(albums)
// mergeAdapter!!.addAdapter(albumAdapter) if (searchResult!!.albums.size > albums.size) {
// if (albums.size > DEFAULT_ALBUMS) { list.add(MoreButton(1, ::expandAlbums))
// moreAlbumsAdapter = mergeAdapter!!.addView(moreAlbumsButton, true)
// }
} }
val songs = searchResult.songs }
val songs = result.songs
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_songs)) list.add(DividerBinder.Divider(R.string.search_songs))
list.addAll(songs) list.addAll(songs)
// if (songs.size > DEFAULT_SONGS) { if (searchResult!!.songs.size > songs.size) {
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) list.add(MoreButton(2, ::expandSongs))
// } }
} }
// Show/hide the empty text view // Show/hide the empty text view
@ -276,35 +253,17 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
viewAdapter.submitList(list) viewAdapter.submitList(list)
} }
// private fun expandArtists() { private fun expandArtists() {
// artistAdapter!!.clear() populateList(listModel.trimResultLength(searchResult!!, maxArtists = Int.MAX_VALUE))
// for (artist in searchResult!!.artists) { }
// artistAdapter!!.add(artist)
// } private fun expandAlbums() {
// artistAdapter!!.notifyDataSetChanged() populateList(listModel.trimResultLength(searchResult!!, maxAlbums = Int.MAX_VALUE))
// mergeAdapter!!.removeAdapter(moreArtistsAdapter) }
// mergeAdapter!!.notifyDataSetChanged()
// } private fun expandSongs() {
// populateList(listModel.trimResultLength(searchResult!!, maxSongs = Int.MAX_VALUE))
// 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 onArtistSelected(artist: Artist) { private fun onArtistSelected(artist: Artist) {
val bundle = Bundle() 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) { override fun onItemClick(item: Identifiable) {
when (item) { when (item) {
is Artist -> { is Artist -> {
@ -464,4 +417,10 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
return true 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 kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R 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.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
@ -48,11 +49,16 @@ import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
/** /**
*
* Displays a group of tracks, eg. the songs of an album, of a playlist etc. * 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") @Suppress("TooManyFunctions")
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() { open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private var albumButtons: View? = null private var albumButtons: View? = null
internal var selectButton: ImageView? = 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() enableButtons()
// Update the buttons when the selection has changed // 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) { if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(entryList, EntryByDiscAndTrackComparator()) 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 val listSize = arguments?.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) ?: 0
// Hide select button for video lists and singular selection lists // 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 (songCount > 0) {
if (listSize == 0 || songCount < listSize) { if (listSize == 0 || songCount < listSize) {
@ -550,12 +565,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
} }
@Suppress("LongMethod") @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 if (args == null) return listModel.currentList
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID) val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false) val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME) 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 playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
val podcastChannelId = args.getString( val podcastChannelId = args.getString(
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
@ -574,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
val albumListOffset = args.getInt( val albumListOffset = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0 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) { listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true refreshListView?.isRefreshing = true
@ -621,7 +635,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onContextMenuItemSelected( override fun onContextMenuItemSelected(
menuItem: MenuItem, menuItem: MenuItem,
item: MusicDirectory.Entry item: MusicDirectory.Child
): Boolean { ): Boolean {
val entryId = item.id val entryId = item.id
@ -673,13 +687,12 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
playAll() playAll()
} }
R.id.menu_item_share -> { R.id.menu_item_share -> {
val entries: MutableList<MusicDirectory.Entry?> = ArrayList(1) if (item is MusicDirectory.Entry) {
entries.add(item)
shareHandler.createShare( shareHandler.createShare(
this, entries, refreshListView, this, listOf(item), refreshListView,
cancellationToken!! cancellationToken!!
) )
return true }
} }
else -> { else -> {
return super.onContextItemSelected(menuItem) return super.onContextItemSelected(menuItem)
@ -688,7 +701,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
return true return true
} }
override fun onItemClick(item: MusicDirectory.Entry) { override fun onItemClick(item: MusicDirectory.Child) {
when { when {
item.isDirectory -> { item.isDirectory -> {
val bundle = Bundle() val bundle = Bundle()
@ -701,7 +714,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
bundle bundle
) )
} }
item.isVideo -> { item is MusicDirectory.Entry && item.isVideo -> {
VideoPlayer.playVideo(requireContext(), item) VideoPlayer.playVideo(requireContext(), item)
} }
else -> { else -> {

View File

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

View File

@ -16,7 +16,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
@ -44,7 +43,7 @@ open class GenericListModel(application: Application) :
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
open fun showSelectFolderHeader(args: Bundle?): Boolean { open fun showSelectFolderHeader(args: Bundle?): Boolean {
return true return false
} }
/** /**
@ -109,20 +108,11 @@ open class GenericListModel(application: Application) :
args: Bundle args: Bundle
) { ) {
// Update the list of available folders if enabled // Update the list of available folders if enabled
// FIXME && refresh ? @Suppress("ComplexCondition")
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) { if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) {
musicFolders.postValue( musicFolders.postValue(
musicService.getMusicFolders(refresh) 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( return SearchResult(
artists = result.artists.take(SearchFragment.DEFAULT_ARTISTS), artists = result.artists.take(maxArtists),
albums = result.albums.take(SearchFragment.DEFAULT_ALBUMS), albums = result.albums.take(maxAlbums),
songs = result.songs.take(SearchFragment.DEFAULT_SONGS) songs = result.songs.take(maxSongs)
) )
} }
} }

View File

@ -21,7 +21,7 @@ import org.moire.ultrasonic.util.Util
*/ */
class TrackCollectionModel(application: Application) : GenericListModel(application) { 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() val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
/* /*
@ -155,6 +155,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
} }
private fun updateList(root: 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( mediaItems.add(
album.title ?: "", album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name) listOf(MEDIA_ALBUM_ITEM, album.id, album.name)

View File

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

View File

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

View File

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

View File

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