mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-16 01:10:36 +03:00
Make SearchResults expandable,
finish music folder support, change Service interface of AlbumList to return listOf(Album)
This commit is contained in:
parent
aa33d7c882
commit
bdac092eff
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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!!)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 -> {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
16
ultrasonic/src/main/res/layout/list_item_more_button.xml
Normal file
16
ultrasonic/src/main/res/layout/list_item_more_button.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/search_more"
|
||||
android:text="@string/search.more"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
</LinearLayout>
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user