From 4e37a2483c11dcb3fc3f397c432073d5a57e4942 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 26 Nov 2021 17:03:33 +0100 Subject: [PATCH] Add an MusicDirectory.Album model to represent the APIAlbum model It became necessary in order to have different types for Tracks vs Albums, instead of just differentiating by isDirectory: Boolean. Also: * Fix Album display in SearchFragment.kt * Use same ids in all lists --- .../moire/ultrasonic/domain/MusicDirectory.kt | 120 +++++++++++++----- .../moire/ultrasonic/domain/SearchResult.kt | 3 +- .../api/subsonic/models/SearchTwoResult.kt | 2 +- .../ultrasonic/util/ShufflePlayBuffer.java | 4 +- .../ultrasonic/adapters/AlbumRowBinder.kt | 12 +- .../ultrasonic/adapters/ArtistRowBinder.kt | 3 + .../moire/ultrasonic/adapters/BaseAdapter.kt | 17 ++- .../ultrasonic/adapters/DividerBinder.kt | 10 +- .../moire/ultrasonic/adapters/ImageHelper.kt | 4 +- .../ultrasonic/adapters/TrackViewHolder.kt | 3 +- .../ultrasonic/domain/APIAlbumConverter.kt | 5 +- .../ultrasonic/domain/APIArtistConverter.kt | 4 + .../ultrasonic/fragment/AlbumListFragment.kt | 16 +-- .../ultrasonic/fragment/ArtistListFragment.kt | 16 +-- .../ultrasonic/fragment/BookmarksFragment.kt | 57 ++++++--- .../ultrasonic/fragment/DownloadsFragment.kt | 18 ++- .../ultrasonic/fragment/MultiListFragment.kt | 29 +++-- .../ultrasonic/fragment/SearchFragment.kt | 71 +++++++---- .../fragment/TrackCollectionFragment.kt | 15 +-- .../ultrasonic/imageloader/ImageLoader.kt | 2 +- .../moire/ultrasonic/model/AlbumListModel.kt | 39 ++---- .../ultrasonic/model/TrackCollectionModel.kt | 32 +---- .../service/AutoMediaBrowserService.kt | 42 +++--- .../ultrasonic/service/CachedMusicService.kt | 29 +++-- .../moire/ultrasonic/service/MusicService.kt | 3 +- .../ultrasonic/service/OfflineMusicService.kt | 22 ++-- .../ultrasonic/service/RESTMusicService.kt | 4 +- .../ultrasonic/subsonic/DownloadHandler.kt | 13 +- .../moire/ultrasonic/util/DragSortCallback.kt | 1 - .../org/moire/ultrasonic/util/FileUtil.kt | 6 +- .../src/main/res/layout/generic_list.xml | 26 +--- .../src/main/res/layout/recycler_view.xml | 35 +++++ ultrasonic/src/main/res/layout/search.xml | 25 +++- .../src/main/res/layout/search_buttons.xml | 45 ------- ultrasonic/src/main/res/layout/track_list.xml | 35 +---- .../domain/APIArtistConverterTest.kt | 2 +- .../domain/APIMusicDirectoryConverterTest.kt | 4 +- .../domain/APIPlaylistConverterTest.kt | 6 +- 38 files changed, 391 insertions(+), 389 deletions(-) create mode 100644 ultrasonic/src/main/res/layout/recycler_view.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 a9c80a5c..49d02a4d 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 @@ -5,71 +5,99 @@ import androidx.room.PrimaryKey import java.io.Serializable import java.util.Date -class MusicDirectory { +class MusicDirectory : ArrayList() { var name: String? = null - private val children = mutableListOf() - fun addAll(entries: Collection) { - children.addAll(entries) + fun addFirst(child: Child) { + add(0, child) } - fun addFirst(child: Entry) { - children.add(0, child) + fun addChild(child: Child) { + add(child) } - fun addChild(child: Entry) { - children.add(child) - } - - fun findChild(id: String): Entry? = children.lastOrNull { it.id == id } - - fun getAllChild(): List = children.toList() + fun findChild(id: String): GenericEntry? = lastOrNull { it.id == id } @JvmOverloads fun getChildren( includeDirs: Boolean = true, includeFiles: Boolean = true - ): List { + ): List { if (includeDirs && includeFiles) { - return children + return toList() } - return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } + return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } } + fun getTracks(): List { + return mapNotNull { + it as? Entry + } + } + + fun getAlbums(): List { + return mapNotNull { + it as? Album + } + } + + abstract class Child : Identifiable, GenericEntry() { + abstract override var id: String + abstract val parent: String? + abstract val isDirectory: Boolean + abstract var album: String? + abstract val title: String? + abstract override val name: String? + abstract val discNumber: Int? + abstract val coverArt: String? + abstract val songCount: Long? + abstract val created: Date? + abstract var artist: String? + abstract val artistId: String? + abstract val duration: Int? + abstract val year: Int? + abstract val genre: String? + abstract var starred: Boolean + abstract val path: String? + abstract var closeness: Int + } + + // TODO: Rename to Track @Entity data class Entry( @PrimaryKey override var id: String, - var parent: String? = null, - var isDirectory: Boolean = false, - var title: String? = null, - var album: String? = null, + override var parent: String? = null, + override var isDirectory: Boolean = false, + override var title: String? = null, + override var album: String? = null, var albumId: String? = null, - var artist: String? = null, - var artistId: String? = null, - var track: Int? = 0, - var year: Int? = 0, - var genre: String? = null, + override var artist: String? = null, + override var artistId: String? = null, + var track: Int? = null, + override var year: Int? = null, + override var genre: String? = null, var contentType: String? = null, var suffix: String? = null, var transcodedContentType: String? = null, var transcodedSuffix: String? = null, - var coverArt: String? = null, + override var coverArt: String? = null, var size: Long? = null, - var songCount: Long? = null, - var duration: Int? = null, + override var songCount: Long? = null, + override var duration: Int? = null, var bitRate: Int? = null, - var path: String? = null, + override var path: String? = null, var isVideo: Boolean = false, - var starred: Boolean = false, - var discNumber: Int? = null, + override var starred: Boolean = false, + override var discNumber: Int? = null, var type: String? = null, - var created: Date? = null, - var closeness: Int = 0, + override var created: Date? = null, + override var closeness: Int = 0, var bookmarkPosition: Int = 0, var userRating: Int? = null, - var averageRating: Float? = null - ) : Serializable, GenericEntry() { + var averageRating: Float? = null, + override var name: String? = null + ) : Serializable, Child() { fun setDuration(duration: Long) { this.duration = duration.toInt() } @@ -94,4 +122,26 @@ class MusicDirectory { override fun compareTo(other: Identifiable) = compareTo(other as Entry) } + + data class Album( + @PrimaryKey override var id: String, + override val parent: String? = null, + override var album: String? = null, + override val title: String? = null, + override val name: String? = null, + override val discNumber: Int = 0, + override val coverArt: String? = null, + override val songCount: Long? = null, + override val created: Date? = null, + override var artist: String? = null, + override val artistId: String? = null, + override val duration: Int = 0, + override val year: Int = 0, + override val genre: String? = null, + override var starred: Boolean = false, + override var path: String? = null, + override var closeness: Int = 0, + ) : Child() { + override val isDirectory = true + } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt index 11c4c97c..7c8ee9bd 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/SearchResult.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.domain +import org.moire.ultrasonic.domain.MusicDirectory.Album import org.moire.ultrasonic.domain.MusicDirectory.Entry /** @@ -7,6 +8,6 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry */ data class SearchResult( val artists: List = listOf(), - val albums: List = listOf(), + val albums: List = listOf(), val songs: List = listOf() ) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt index 1b42f640..5e94c22a 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SearchTwoResult.kt @@ -4,6 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty data class SearchTwoResult( @JsonProperty("artist") val artistList: List = emptyList(), - @JsonProperty("album") val albumList: List = emptyList(), + @JsonProperty("album") val albumList: List = emptyList(), @JsonProperty("song") val songList: List = emptyList() ) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java index d918a4e0..156ea868 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java @@ -100,8 +100,8 @@ public class ShufflePlayBuffer synchronized (buffer) { - buffer.addAll(songs.getChildren()); - Timber.i("Refilled shuffle play buffer with %d songs.", songs.getChildren().size()); + buffer.addAll(songs.getTracks()); + Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size()); } } catch (Exception x) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt index df0fa70e..c7ce06d0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt @@ -31,11 +31,11 @@ import timber.log.Timber * Creates a Row in a RecyclerView which contains the details of an Album */ class AlbumRowBinder( - val onItemClick: (MusicDirectory.Entry) -> Unit, - val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, + val onItemClick: (MusicDirectory.Album) -> Unit, + val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean, private val imageLoader: ImageLoader, - context: Context, -) : ItemViewBinder(), KoinComponent { + context: Context +) : ItemViewBinder(), KoinComponent { private val starDrawable: Drawable = Util.getDrawableFromAttribute(context, R.attr.star_full) @@ -46,7 +46,7 @@ class AlbumRowBinder( val layout = R.layout.album_list_item val contextMenuLayout = R.menu.artist_context_menu - override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Entry) { + override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Album) { holder.album.text = item.title holder.artist.text = item.artist holder.details.setOnClickListener { onItemClick(item) } @@ -86,7 +86,7 @@ class AlbumRowBinder( /** * Handles the star / unstar action for an album */ - private fun onStarClick(entry: MusicDirectory.Entry, star: ImageView) { + private fun onStarClick(entry: MusicDirectory.Album, star: ImageView) { entry.starred = !entry.starred star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) val musicService = getMusicService() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index f850e037..41a2e7a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -14,6 +14,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder import org.koin.core.component.KoinComponent @@ -31,6 +32,7 @@ class ArtistRowBinder( val onItemClick: (ArtistOrIndex) -> Unit, val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, + private val enableSections: Boolean = true ) : ItemViewBinder(), KoinComponent { val layout = R.layout.artist_list_item @@ -39,6 +41,7 @@ class ArtistRowBinder( override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { holder.textView.text = item.name holder.section.text = getSectionForArtist(item) + holder.section.isVisible = enableSections holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnLongClickListener { val popup = Helper.createPopupMenu(holder.itemView) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 3f91a6d5..6e53062a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -1,3 +1,10 @@ +/* + * BaseAdapter.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.adapters import android.annotation.SuppressLint @@ -8,10 +15,15 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.DiffUtil import com.drakeet.multitype.MultiTypeAdapter -import java.util.TreeSet import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.util.BoundedTreeSet +/** + * The BaseAdapter which extends the MultiTypeAdapter from an external library. + * It provides selection support as well as Diffing the submitted lists for performance. + * + * It should be kept generic enought that it can be used a Base for all lists in the app. + */ class BaseAdapter : MultiTypeAdapter() { // Update the BoundedTreeSet if selection type is changed @@ -34,11 +46,12 @@ class BaseAdapter : MultiTypeAdapter() { return getItem(position).longId } - private fun getItem(position: Int): T { return mDiffer.currentList[position] } + // override getIt + override var items: List get() = getCurrentList() set(value) { 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 3228ee5a..c8dcf395 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/DividerBinder.kt @@ -9,12 +9,10 @@ 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 DividerBinder: ItemViewBinder() { - +class DividerBinder : ItemViewBinder() { // Set our layout files val layout = R.layout.row_divider @@ -39,7 +37,7 @@ class DividerBinder: ItemViewBinder.toDomainEntityList(): List = this.map { it.toDomainEntity() } +fun List.toDomainEntityList(): List = this.map { it.toDomainEntity() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt index 51c2c72f..4c2294ba 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -23,3 +23,7 @@ fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory(). name = this@toMusicDirectoryDomainEntity.name addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) } + +fun APIArtist.toDomainEntityList(): List { + return this.albumsList.map { it.toDomainEntity() } +} 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 cc11a99b..49df1d34 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -16,7 +16,7 @@ import org.moire.ultrasonic.util.Constants * Displays a list of Albums from the media library * FIXME: Add music folder support */ -class AlbumListFragment : EntryListFragment() { +class AlbumListFragment : EntryListFragment() { /** * The ViewModel to use to get the data @@ -28,16 +28,6 @@ class AlbumListFragment : EntryListFragment() { */ override val mainLayout: Int = R.layout.generic_list - /** - * The id of the refresh view - */ - override val refreshListId: Int = R.id.generic_list_refresh - - /** - * The id of the RecyclerView - */ - override val recyclerViewId = R.id.generic_list_recycler - /** * The id of the target in the navigation graph where we should go, * after the user has clicked on an item @@ -47,7 +37,7 @@ class AlbumListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?): LiveData> { if (args == null) throw IllegalArgumentException("Required arguments are missing") val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) @@ -83,7 +73,7 @@ class AlbumListFragment : EntryListFragment() { ) } - override fun onItemClick(item: MusicDirectory.Entry) { + override fun onItemClick(item: MusicDirectory.Album) { val bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) 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 10b9c7ee..d3169e1e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -27,16 +27,6 @@ class ArtistListFragment : EntryListFragment() { */ override val mainLayout = R.layout.generic_list - /** - * The id of the refresh view - */ - override val refreshListId = R.id.generic_list_refresh - - /** - * The id of the RecyclerView - */ - override val recyclerViewId = R.id.generic_list_recycler - /** * The id of the target in the navigation graph where we should go, * after the user has clicked on an item @@ -69,8 +59,10 @@ class ArtistListFragment : EntryListFragment() { bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name) bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist)) - bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, - Constants.ALPHABETICAL_BY_NAME) + bundle.putString( + Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, + Constants.ALPHABETICAL_BY_NAME + ) bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.name) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 1000) bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) 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 5cc077cb..54dfcccf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -1,14 +1,19 @@ +/* + * BookmarksFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle import android.view.View -import androidx.core.view.isVisible import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -20,7 +25,7 @@ import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle * * Therefore this fragment allows only for singular selection and playback. * - * // FIXME: use restore for playback + * FIXME: use restore for playback */ class BookmarksFragment : TrackCollectionFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -31,14 +36,6 @@ class BookmarksFragment : TrackCollectionFragment() { viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE } - override fun setupButtons(view: View) { - super.setupButtons(view) - - // Hide select all button - //selectButton?.visibility = View.GONE - //moreButton?.visibility = View.GONE - } - override fun getLiveData(args: Bundle?): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true @@ -47,12 +44,34 @@ class BookmarksFragment : TrackCollectionFragment() { } return listModel.currentList } + + /** + * Set a custom listener to perform the playing, in order to be able to restore + * the playback position + */ + override fun setupButtons(view: View) { + super.setupButtons(view) + + playNowButton!!.setOnClickListener { + playNow(getSelectedSongs()) + } + } + + /** + * Custom playback function which uses the restore functionality. A bit of a hack.. + */ + private fun playNow(songs: List) { + if (songs.isNotEmpty()) { + + val position = songs[0].bookmarkPosition + + mediaPlayerController.restore( + songs = songs, + currentPlayingIndex = 0, + currentPlayingPosition = position, + autoPlay = true, + newPlaylist = true + ) + } + } } - - - - - - - - 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 86dc63af..70c70986 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -1,3 +1,10 @@ +/* + * DownloadsFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.app.Application @@ -14,6 +21,13 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.util.Util +/** + * Displays currently running downloads. + * For now its a read-only view, there are no manipulations of the download list possible. + * + * A consideration would be to base this class on TrackCollectionFragment and thereby inheriting the + * buttons useful to manipulate the list. + */ class DownloadsFragment : MultiListFragment() { /** @@ -60,7 +74,9 @@ class DownloadsFragment : MultiListFragment() { ) ) - viewAdapter.submitList(listModel.getList().value) + val liveDataList = listModel.getList() + + viewAdapter.submitList(liveDataList.value) } } 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 8d6725a7..635f7632 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -1,3 +1,10 @@ +/* + * MultiListFragment.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.fragment import android.os.Bundle @@ -5,6 +12,8 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData @@ -37,6 +46,7 @@ abstract class MultiListFragment : Fragment() { protected var refreshListView: SwipeRefreshLayout? = null internal var listView: RecyclerView? = null internal lateinit var viewManager: LinearLayoutManager + internal lateinit var emptyTextView: TextView /** * The Adapter for the RecyclerView @@ -76,14 +86,11 @@ abstract class MultiListFragment : Fragment() { open val mainLayout: Int = R.layout.generic_list /** - * The id of the refresh view + * The ids of the swipe refresh view, the recycler view and the empty text view */ - open val refreshListId: Int = R.id.generic_list_refresh - - /** - * The id of the RecyclerView - */ - open val recyclerViewId = R.id.generic_list_recycler + open val refreshListId = R.id.swipe_refresh_view + open val recyclerViewId = R.id.recycler_view + open val emptyTextViewId = R.id.empty_list_text open fun setTitle(title: String?) { if (title == null) { @@ -113,11 +120,15 @@ abstract class MultiListFragment : Fragment() { // Populate the LiveData. This starts an API request in most cases liveDataItems = getLiveData(arguments) + // Link view to display text if the list is empty + // FIXME: Hook this up globally. + emptyTextView = view.findViewById(emptyTextViewId) + // Register an observer to update our UI when the data changes liveDataItems.observe( viewLifecycleOwner, - { - newItems -> + { newItems -> + emptyTextView.isVisible = newItems.isEmpty() viewAdapter.submitList(newItems) } ) 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 76002c05..19553929 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -12,8 +12,8 @@ import android.view.MenuItem import android.view.View import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.ListAdapter -import android.widget.TextView import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject 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.TrackViewBinder @@ -48,10 +49,6 @@ import timber.log.Timber * Initiates a search on the media library and displays the results */ class SearchFragment : MultiListFragment(), KoinComponent { - private var artistsHeading: View? = null - private var albumsHeading: View? = null - private var songsHeading: View? = null - private var notFound: TextView? = null private var moreArtistsButton: View? = null private var moreAlbumsButton: View? = null private var moreSongsButton: View? = null @@ -71,8 +68,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { override val listModel: SearchListModel by viewModels() - override val recyclerViewId = R.id.search_list - override val mainLayout: Int = R.layout.search override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -87,10 +82,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) if (buttons != null) { - artistsHeading = buttons.findViewById(R.id.search_artists) - albumsHeading = buttons.findViewById(R.id.search_albums) - songsHeading = buttons.findViewById(R.id.search_songs) - notFound = buttons.findViewById(R.id.search_not_found) moreArtistsButton = buttons.findViewById(R.id.search_more_artists) moreAlbumsButton = buttons.findViewById(R.id.search_more_albums) moreSongsButton = buttons.findViewById(R.id.search_more_songs) @@ -103,7 +94,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } ) - searchRefresh = view.findViewById(R.id.search_entries_refresh) + searchRefresh = view.findViewById(R.id.swipe_refresh_view) searchRefresh!!.isEnabled = false // list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long -> @@ -132,6 +123,37 @@ class SearchFragment : MultiListFragment(), KoinComponent { registerForContextMenu(listView!!) + // Register our data binders + // IMPORTANT: + // They need to be added in the order of most specific -> least specific. + viewAdapter.register( + ArtistRowBinder( + onItemClick = { entry -> onItemClick(entry) }, + onContextMenuClick = { menuItem, entry -> + onContextMenuItemSelected( + menuItem, + entry + ) + }, + imageLoader = imageLoaderProvider.getImageLoader(), + enableSections = false + ) + ) + + viewAdapter.register( + AlbumRowBinder( + onItemClick = { entry -> onItemClick(entry) }, + onContextMenuClick = { menuItem, entry -> + onContextMenuItemSelected( + menuItem, + entry + ) + }, + imageLoader = imageLoaderProvider.getImageLoader(), + context = requireContext() + ) + ) + viewAdapter.register( TrackViewBinder( checkable = false, @@ -141,14 +163,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) ) - viewAdapter.register( - ArtistRowBinder( - { entry -> onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider.getImageLoader() - ) - ) - viewAdapter.register( DividerBinder() ) @@ -164,7 +178,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { } // Fragment was started from the Menu, create empty list - populateList(SearchResult()) + // populateList(SearchResult()) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -180,11 +194,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { val autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) + // If started with a query, enter it to the searchView if (query != null) { searchView.setQuery(query, false) searchView.clearFocus() } + searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { override fun onSuggestionSelect(position: Int): Boolean { return true @@ -423,8 +439,9 @@ class SearchFragment : MultiListFragment(), 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 } } @@ -454,17 +471,15 @@ class SearchFragment : MultiListFragment(), KoinComponent { } val songs = searchResult.songs if (songs.isNotEmpty()) { - list.add(DividerBinder.Divider(R.string.search_albums)) + list.add(DividerBinder.Divider(R.string.search_songs)) list.addAll(songs) // if (songs.size > DEFAULT_SONGS) { // moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) // } } - // FIXME - if (list.isEmpty()) { - // mergeAdapter!!.addView(notFound, false) - } + // Show/hide the empty text view + emptyTextView.isVisible = list.isEmpty() viewAdapter.submitList(list) } @@ -506,7 +521,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { // Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) // } - private fun onAlbumSelected(album: MusicDirectory.Entry, autoplay: Boolean) { + private fun onAlbumSelected(album: MusicDirectory.Album, autoplay: Boolean) { val bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id) bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) 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 5c9687f4..6d15cc8d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -30,7 +30,6 @@ 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.BaseAdapter import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -85,16 +84,6 @@ open class TrackCollectionFragment : MultiListFragment() { */ override val mainLayout: Int = R.layout.track_list - /** - * The id of the refresh view - */ - override val refreshListId: Int = R.id.generic_list_refresh - - /** - * The id of the RecyclerView - */ - override val recyclerViewId = R.id.generic_list_recycler - /** * The id of the target in the navigation graph where we should go, * after the user has clicked on an item @@ -118,7 +107,7 @@ open class TrackCollectionFragment : MultiListFragment() { setupButtons(view) - emptyView = view.findViewById(R.id.select_album_empty) + emptyView = view.findViewById(R.id.empty_list_text) registerForContextMenu(listView!!) setHasOptionsMenu(true) @@ -629,7 +618,7 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.currentListIsSortable = true } - private fun getSelectedSongs(): List { + internal fun getSelectedSongs(): List { // Walk through selected set and get the Entries based on the saved ids. return viewAdapter.getCurrentList().mapNotNull { if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId)) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 4b28e82c..09f6dc3c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -87,7 +87,7 @@ class ImageLoader( @JvmOverloads fun loadImage( view: View?, - entry: MusicDirectory.Entry?, + entry: MusicDirectory.Child?, large: Boolean, size: Int, defaultResourceId: Int = R.drawable.unknown_album 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 6c28f94c..24dfd80e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -5,7 +5,6 @@ import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicService @@ -14,7 +13,7 @@ import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { - val list: MutableLiveData> = MutableLiveData(listOf()) + val list: MutableLiveData> = MutableLiveData(listOf()) var lastType: String? = null private var loadedUntil: Int = 0 @@ -22,7 +21,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { refresh: Boolean, swipe: SwipeRefreshLayout, args: Bundle - ): LiveData> { + ): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! @@ -35,29 +34,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { } fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) { - - var root = MusicDirectory() - val musicDirectory = musicService.getArtist(id, name, refresh) - - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) - - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) - - root.addFirst(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } - list.postValue(root.getChildren()) + list.postValue(musicService.getArtist(id, name, refresh)) } override fun load( @@ -108,13 +85,15 @@ 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() + val list = ArrayList() list.addAll(this.list.value!!) - list.addAll(musicDirectory.getAllChild()) - this.list.postValue(list) + list.addAll(musicDirectory.getChildren()) + this.list.postValue(list as List) } else { - list.postValue(musicDirectory.getAllChild()) + list.postValue(musicDirectory.getChildren() as List) } loadedUntil = offset 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 ee5b4e27..ac9ef7c1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -12,7 +12,6 @@ import androidx.lifecycle.MutableLiveData import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings @@ -54,25 +53,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } else { val musicDirectory = service.getMusicDirectory(id, name, refresh) - - if (Settings.shouldShowAllSongsByArtist && - musicDirectory.findChild(allSongsId) == null && - hasOnlyFolders(musicDirectory) - ) { - val allSongs = MusicDirectory.Entry(allSongsId) - - allSongs.isDirectory = true - allSongs.artist = name - allSongs.parent = id - allSongs.title = String.format( - context.resources.getString(R.string.select_album_all_songs), name - ) - - root.addChild(allSongs) - root.addAll(musicDirectory.getChildren()) - } else { - root = musicDirectory - } + root = musicDirectory } currentDirectory.postValue(root) @@ -87,13 +68,13 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat ) { val service = MusicServiceFactory.getMusicService() - for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { + for (song in parent.getTracks()) { if (!song.isVideo && !song.isDirectory) { songs.add(song) } } - for ((id1, _, _, title) in parent.getChildren(true, includeFiles = false)) { + for ((id1, _, _, title) in parent.getAlbums()) { var root: MusicDirectory if (allSongsId != id1) { @@ -118,13 +99,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val songs: MutableCollection = LinkedList() val artist = service.getArtist(parentId, "", false) - for ((id1) in artist.getChildren()) { + // FIXME is still working? + for ((id1) in artist) { if (allSongsId != id1) { val albumDirectory = service.getAlbum( id1, "", false ) - for (song in albumDirectory.getChildren()) { + for (song in albumDirectory.getTracks()) { if (!song.isVideo) { songs.add(song) } @@ -252,6 +234,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getChildren()) + currentList.postValue(root.getTracks()) } } 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 173c5806..c44d202f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -484,10 +484,12 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val albums = if (!isOffline && useId3Tags) { callWithErrorHandling { musicService.getArtist(id, name, false) } } else { - callWithErrorHandling { musicService.getMusicDirectory(id, name, false) } + callWithErrorHandling { + musicService.getMusicDirectory(id, name, false).getAlbums() + } } - albums?.getAllChild()?.map { album -> + albums?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) @@ -517,7 +519,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? - val items = songs.getChildren().take(DISPLAY_LIMIT) + val items = songs.getTracks().take(DISPLAY_LIMIT) items.map { item -> if (item.isDirectory) mediaItems.add( @@ -573,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - albums?.getAllChild()?.map { album -> + albums?.getChildren()?.map { album -> mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) @@ -582,7 +584,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { ) } - if (albums?.getAllChild()?.count() ?: 0 >= DISPLAY_LIMIT) + if (albums?.getChildren()?.count() ?: 0 >= DISPLAY_LIMIT) mediaItems.add( R.string.search_more, listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), @@ -624,13 +626,13 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } if (content != null) { - if (content.getAllChild().count() > 1) + if (content.getChildren().count() > 1) mediaItems.addPlayAllItem( listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|") ) // Playlist should be cached as it may contain random elements - playlistCache = content.getAllChild() + playlistCache = content.getTracks() playlistCache!!.take(DISPLAY_LIMIT).map { item -> mediaItems.add( MediaBrowserCompat.MediaItem( @@ -657,7 +659,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (playlistCache == null) { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them val content = callWithErrorHandling { musicService.getPlaylist(id, name) } - playlistCache = content?.getAllChild() + playlistCache = content?.getTracks() } if (playlistCache != null) playSongs(playlistCache) } @@ -668,7 +670,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (playlistCache == null) { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them val content = callWithErrorHandling { musicService.getPlaylist(id, name) } - playlistCache = content?.getAllChild() + playlistCache = content?.getTracks() } val song = playlistCache?.firstOrNull { x -> x.id == songId } if (song != null) playSong(song) @@ -678,14 +680,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private fun playAlbum(id: String, name: String) { serviceScope.launch { val songs = listSongsInMusicService(id, name) - if (songs != null) playSongs(songs.getAllChild()) + if (songs != null) playSongs(songs.getTracks()) } } private fun playAlbumSong(id: String, name: String, songId: String) { serviceScope.launch { val songs = listSongsInMusicService(id, name) - val song = songs?.getAllChild()?.firstOrNull { x -> x.id == songId } + val song = songs?.getTracks()?.firstOrNull { x -> x.id == songId } if (song != null) playSong(song) } } @@ -717,10 +719,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { - if (episodes.getAllChild().count() > 1) + if (episodes.getTracks().count() > 1) mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|")) - episodes.getAllChild().map { episode -> + episodes.getTracks().map { episode -> mediaItems.add( MediaBrowserCompat.MediaItem( Util.getMediaDescriptionForEntry( @@ -741,7 +743,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { serviceScope.launch { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { - playSongs(episodes.getAllChild()) + playSongs(episodes.getTracks()) } } } @@ -751,7 +753,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { val selectedEpisode = episodes - .getAllChild() + .getTracks() .firstOrNull { episode -> episode.id == episodeId } if (selectedEpisode != null) playSong(selectedEpisode) } @@ -766,7 +768,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) - songs.getAllChild().map { song -> + songs.getTracks().map { song -> mediaItems.add( MediaBrowserCompat.MediaItem( Util.getMediaDescriptionForEntry( @@ -787,7 +789,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val bookmarks = callWithErrorHandling { musicService.getBookmarks() } if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) - val song = songs.getAllChild().firstOrNull { song -> song.id == id } + val song = songs.getTracks().firstOrNull { song -> song.id == id } if (song != null) playSong(song) } } @@ -926,11 +928,11 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } if (songs != null) { - if (songs.getAllChild().count() > 1) + if (songs.getChildren().count() > 1) mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? - val items = songs.getAllChild() + val items = songs.getTracks() randomSongsCache = items items.map { song -> mediaItems.add( @@ -954,7 +956,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { // This can only happen if Android Auto cached items, but Ultrasonic has forgot them // In this case we request a new set of random songs val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } - randomSongsCache = content?.getAllChild() + randomSongsCache = content?.getTracks() } if (randomSongsCache != null) playSongs(randomSongsCache) } 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 fed88a9b..bca7aa51 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -41,7 +41,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, // Old style TimeLimitedCache private val cachedMusicDirectories: LRUCache> - private val cachedArtist: LRUCache> + private val cachedArtist: LRUCache>> private val cachedAlbum: LRUCache> private val cachedUserInfo: LRUCache> private val cachedLicenseValid = TimeLimitedCache(120, TimeUnit.SECONDS) @@ -148,20 +148,21 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { - checkSettingsChanged() - var cache = if (refresh) null else cachedArtist[id] - var dir = cache?.get() - if (dir == null) { - dir = musicService.getArtist(id, name, refresh) - cache = TimeLimitedCache( - Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS - ) - cache.set(dir) - cachedArtist.put(id, cache) + override fun getArtist(id: String, name: String?, refresh: Boolean): + List { + checkSettingsChanged() + var cache = if (refresh) null else cachedArtist[id] + var dir = cache?.get() + if (dir == null) { + dir = musicService.getArtist(id, name, refresh) + cache = TimeLimitedCache( + Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS + ) + cache.set(dir) + cachedArtist.put(id, cache) + } + return dir } - return dir - } @Throws(Exception::class) override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { 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 902ab3f9..5d78644f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -24,6 +24,7 @@ import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo @Suppress("TooManyFunctions") + interface MusicService { @Throws(Exception::class) fun ping() @@ -56,7 +57,7 @@ interface MusicService { fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory @Throws(Exception::class) - fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory + fun getArtist(id: String, name: String?, refresh: Boolean): List @Throws(Exception::class) fun getAlbum(id: String, name: String?, refresh: Boolean): 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 9ec622b7..d1b26e5c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -14,7 +14,6 @@ import java.io.FileReader import java.io.FileWriter import java.io.InputStream import java.io.Reader -import java.lang.Math.min import java.util.ArrayList import java.util.HashSet import java.util.LinkedList @@ -119,7 +118,7 @@ class OfflineMusicService : MusicService, KoinComponent { override fun search(criteria: SearchCriteria): SearchResult { val artists: MutableList = ArrayList() - val albums: MutableList = ArrayList() + val albums: MutableList = ArrayList() val songs: MutableList = ArrayList() val root = FileUtil.musicDirectory var closeness: Int @@ -258,7 +257,7 @@ class OfflineMusicService : MusicService, KoinComponent { return result } children.shuffle() - val finalSize: Int = min(children.size, size) + val finalSize: Int = children.size.coerceAtMost(size) for (i in 0 until finalSize) { val file = children[i % children.size] result.addChild(createEntry(file, getName(file))) @@ -447,9 +446,10 @@ class OfflineMusicService : MusicService, KoinComponent { } @Throws(OfflineException::class) - override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { - throw OfflineException("getArtist isn't available in offline mode") - } + override fun getArtist(id: String, name: String?, refresh: Boolean): + List { + throw OfflineException("getArtist isn't available in offline mode") + } @Throws(OfflineException::class) override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { @@ -498,7 +498,7 @@ class OfflineMusicService : MusicService, KoinComponent { } @Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth") - private fun createEntry(file: File, name: String?): MusicDirectory.Entry { + private fun createEntry(file: File, name: String?): MusicDirectory.Child { val entry = MusicDirectory.Entry(file.path) entry.isDirectory = file.isDirectory entry.parent = file.parent @@ -600,7 +600,7 @@ class OfflineMusicService : MusicService, KoinComponent { artistName: String, file: File, criteria: SearchCriteria, - albums: MutableList, + albums: MutableList, songs: MutableList ) { var closeness: Int @@ -611,7 +611,7 @@ class OfflineMusicService : MusicService, KoinComponent { val album = createEntry(albumFile, albumName) album.artist = artistName album.closeness = closeness - albums.add(album) + albums.add(album as MusicDirectory.Album) } for (songFile in FileUtil.listMediaFiles(albumFile)) { val songName = getName(songFile) @@ -622,7 +622,7 @@ class OfflineMusicService : MusicService, KoinComponent { song.artist = artistName song.album = albumName song.closeness = closeness - songs.add(song) + songs.add(song as MusicDirectory.Entry) } } } else { @@ -632,7 +632,7 @@ class OfflineMusicService : MusicService, KoinComponent { song.artist = artistName song.album = songName song.closeness = closeness - songs.add(song) + songs.add(song as MusicDirectory.Entry) } } } 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 3121fbe6..9ce8f92b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -143,10 +143,10 @@ open class RESTMusicService( id: String, name: String?, refresh: Boolean - ): MusicDirectory { + ): List { val response = API.getArtist(id).execute().throwOnFailure() - return response.body()!!.artist.toMusicDirectoryDomainEntity() + return response.body()!!.artist.toDomainEntityList() } @Throws(Exception::class) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 1d14d2fb..7ae5ba08 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -240,18 +240,13 @@ class DownloadHandler( if (songs.size > maxSongs) { return } - for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { + for (song in parent.getTracks()) { if (!song.isVideo) { songs.add(song) } } val musicService = getMusicService() - for ( - (id1, _, _, title) in parent.getChildren( - includeDirs = true, - includeFiles = false - ) - ) { + for ((id1, _, _, title) in parent.getAlbums()) { val root: MusicDirectory = if ( !isOffline() && Settings.shouldUseId3Tags @@ -271,13 +266,13 @@ class DownloadHandler( } val musicService = getMusicService() val artist = musicService.getArtist(id, "", false) - for ((id1) in artist.getChildren()) { + for ((id1) in artist) { val albumDirectory = musicService.getAlbum( id1, "", false ) - for (song in albumDirectory.getChildren()) { + for (song in albumDirectory.getTracks()) { if (!song.isVideo) { songs.add(song) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt index 91894874..f090d9d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DragSortCallback.kt @@ -5,7 +5,6 @@ import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.RecyclerView import org.moire.ultrasonic.adapters.BaseAdapter -import timber.log.Timber class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index f9a03051..902ad61f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -119,7 +119,7 @@ object FileUtil { * @param large Whether to get the key for the large or the default image * @return String The hash key */ - fun getAlbumArtKey(entry: MusicDirectory.Entry?, large: Boolean): String? { + fun getAlbumArtKey(entry: MusicDirectory.Child?, large: Boolean): String? { if (entry == null) return null val albumDir = getAlbumDirectory(entry) return getAlbumArtKey(albumDir, large) @@ -190,7 +190,7 @@ object FileUtil { return albumArtDir } - fun getAlbumDirectory(entry: MusicDirectory.Entry): File { + fun getAlbumDirectory(entry: MusicDirectory.Child): File { val dir: File if (!TextUtils.isEmpty(entry.path)) { val f = File(fileSystemSafeDir(entry.path)) @@ -457,7 +457,7 @@ object FileUtil { try { fw.write("#EXTM3U\n") - for (e in playlist.getChildren()) { + for (e in playlist.getTracks()) { var filePath = getSongFile(e).absolutePath if (!File(filePath).exists()) { diff --git a/ultrasonic/src/main/res/layout/generic_list.xml b/ultrasonic/src/main/res/layout/generic_list.xml index 1cb9529d..45bce488 100644 --- a/ultrasonic/src/main/res/layout/generic_list.xml +++ b/ultrasonic/src/main/res/layout/generic_list.xml @@ -2,32 +2,8 @@ - - - - - + diff --git a/ultrasonic/src/main/res/layout/recycler_view.xml b/ultrasonic/src/main/res/layout/recycler_view.xml new file mode 100644 index 00000000..09ca5ccc --- /dev/null +++ b/ultrasonic/src/main/res/layout/recycler_view.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search.xml b/ultrasonic/src/main/res/layout/search.xml index a5132493..391d278f 100644 --- a/ultrasonic/src/main/res/layout/search.xml +++ b/ultrasonic/src/main/res/layout/search.xml @@ -1,20 +1,31 @@ + a:layout_width="fill_parent" + a:layout_height="fill_parent" + a:orientation="vertical"> + + - + a:layout_weight="1.0" /> diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml index 66b82755..1666bdd1 100644 --- a/ultrasonic/src/main/res/layout/search_buttons.xml +++ b/ultrasonic/src/main/res/layout/search_buttons.xml @@ -4,51 +4,6 @@ a:layout_width="fill_parent" a:layout_height="wrap_content"> - - - - - - - - - - - - - - - + diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt index 37c8f5c9..4ae7528b 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIArtistConverterTest.kt @@ -42,7 +42,7 @@ class APIArtistConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild() `should be equal to` entity.albumsList + getChildren() `should be equal to` entity.albumsList .map { it.toDomainEntity() }.toMutableList() } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt index e03da15d..938d1807 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverterTest.kt @@ -24,8 +24,8 @@ class APIMusicDirectoryConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild().size `should be equal to` entity.childList.size - getAllChild() `should be equal to` entity.childList + getChildren().size `should be equal to` entity.childList.size + getChildren() `should be equal to` entity.childList .map { it.toDomainEntity() }.toMutableList() } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt index 041c71cd..7a5ed282 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIPlaylistConverterTest.kt @@ -26,9 +26,9 @@ class APIPlaylistConverterTest { with(convertedEntity) { name `should be equal to` entity.name - getAllChild().size `should be equal to` entity.entriesList.size - getAllChild()[0] `should be equal to` entity.entriesList[0].toDomainEntity() - getAllChild()[1] `should be equal to` entity.entriesList[1].toDomainEntity() + getChildren().size `should be equal to` entity.entriesList.size + getChildren()[0] `should be equal to` entity.entriesList[0].toDomainEntity() + getChildren()[1] `should be equal to` entity.entriesList[1].toDomainEntity() } }