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> -