From bdac092eff4e8f2d21c794b8e9dc2ca0661c53aa Mon Sep 17 00:00:00 2001
From: tzugen <tzugen@riseup.net>
Date: Tue, 30 Nov 2021 00:46:48 +0100
Subject: [PATCH] Make SearchResults expandable, finish music folder support,
 change Service interface of AlbumList to return listOf(Album)

---
 .../moire/ultrasonic/domain/MusicDirectory.kt |   4 +-
 .../subsonic/response/GetAlbumListResponse.kt |   6 +-
 .../util/EntryByDiscAndTrackComparator.java   |  72 ----------
 .../moire/ultrasonic/adapters}/AlbumHeader.kt |   8 +-
 .../ultrasonic/adapters/DividerBinder.kt      |   1 +
 .../ultrasonic/adapters/HeaderViewBinder.kt   |   1 -
 .../ultrasonic/adapters/MoreButtonBinder.kt   |  50 +++++++
 .../ultrasonic/domain/APIAlbumConverter.kt    |   3 +-
 .../ultrasonic/fragment/AlbumListFragment.kt  |  40 +-----
 .../ultrasonic/fragment/ArtistListFragment.kt |  10 +-
 .../ultrasonic/fragment/BookmarksFragment.kt  |   3 +-
 .../ultrasonic/fragment/DownloadsFragment.kt  |   2 +-
 .../ultrasonic/fragment/EntryListFragment.kt  |  40 +++++-
 .../ultrasonic/fragment/MultiListFragment.kt  |   4 +-
 .../ultrasonic/fragment/SearchFragment.kt     | 129 ++++++------------
 .../fragment/TrackCollectionFragment.kt       |  51 ++++---
 .../moire/ultrasonic/model/AlbumListModel.kt  |  30 ++--
 .../ultrasonic/model/GenericListModel.kt      |  16 +--
 .../moire/ultrasonic/model/SearchListModel.kt |  13 +-
 .../ultrasonic/model/TrackCollectionModel.kt  |  16 +--
 .../service/AutoMediaBrowserService.kt        |   2 +-
 .../ultrasonic/service/CachedMusicService.kt  |   4 +-
 .../moire/ultrasonic/service/MusicService.kt  |   9 +-
 .../ultrasonic/service/OfflineMusicService.kt |  27 ++--
 .../ultrasonic/service/RESTMusicService.kt    |  14 +-
 .../util/EntryByDiscAndTrackComparator.kt     |  50 +++++++
 .../main/res/layout/list_item_more_button.xml |  16 +++
 .../src/main/res/layout/search_buttons.xml    |  39 ------
 28 files changed, 319 insertions(+), 341 deletions(-)
 delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java
 rename ultrasonic/src/main/{java/org/moire/ultrasonic/util => kotlin/org/moire/ultrasonic/adapters}/AlbumHeader.kt (91%)
 create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MoreButtonBinder.kt
 create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt
 create mode 100644 ultrasonic/src/main/res/layout/list_item_more_button.xml
 delete mode 100644 ultrasonic/src/main/res/layout/search_buttons.xml

diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt
index c8c319d7..b409acab 100644
--- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt
+++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt
@@ -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
     }
 }
diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt
index 8e3ca708..81c6be5b 100644
--- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt
+++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetAlbumListResponse.kt
@@ -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()
 )
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java
deleted file mode 100644
index cbf91c91..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.java
+++ /dev/null
@@ -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);
-	}
-}
\ No newline at end of file
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt
similarity index 91%
rename from ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt
rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt
index c049d49c..978ead6f 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/AlbumHeader.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumHeader.kt
@@ -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) {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt
index 679839a7..b4f4627c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt
@@ -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
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt
index d851c37e..7032864f 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt
@@ -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
 
 /**
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MoreButtonBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MoreButtonBinder.kt
new file mode 100644
index 00000000..03ee49bb
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/MoreButtonBinder.kt
@@ -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)
+    }
+
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt
index 6fc540a2..acccda31 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt
@@ -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,
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
index 30a0a601..e719e8f8 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt
@@ -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
-    }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
index 1ec67ac3..73346de6 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt
@@ -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!!)
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
index 9fdcfa5c..8f575ce7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt
@@ -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()
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
index 1ad3c658..86847435 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt
@@ -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()
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
index 44dc1aef..00726af2 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt
@@ -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(
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
index 5588cebb..d0230a65 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
index a9377cda..ded120e4 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
@@ -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
+    }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
index 9fa39b9c..5cd254a5 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
@@ -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 -> {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
index 7ac20f1a..0b935e36 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt
@@ -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
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
index 1919c7be..c03880fe 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt
@@ -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
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
index a5907352..252c48cb 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt
@@ -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)
         )
     }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt
index a3eebe3d..3b658ae6 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt
@@ -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())
     }
 }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
index a8c8b91c..02f897fc 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
index bca7aa51..7ebbdd11 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
@@ -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)
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
index 5d78644f..410558b8 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
@@ -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
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
index 7714a0ea..fad431f7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
@@ -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) {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
index 3f89c136..545fab3d 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt
new file mode 100644
index 00000000..ec3ef5cd
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/EntryByDiscAndTrackComparator.kt
@@ -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)
+        }
+    }
+}
diff --git a/ultrasonic/src/main/res/layout/list_item_more_button.xml b/ultrasonic/src/main/res/layout/list_item_more_button.xml
new file mode 100644
index 00000000..8d9b886c
--- /dev/null
+++ b/ultrasonic/src/main/res/layout/list_item_more_button.xml
@@ -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>
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml
deleted file mode 100644
index 1666bdd1..00000000
--- a/ultrasonic/src/main/res/layout/search_buttons.xml
+++ /dev/null
@@ -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>
-