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
This commit is contained in:
tzugen 2021-11-26 17:03:33 +01:00
parent 5dfb66eec2
commit 4e37a2483c
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
38 changed files with 391 additions and 389 deletions

View File

@ -5,71 +5,99 @@ import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Date
class MusicDirectory {
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
var name: String? = null
private val children = mutableListOf<Entry>()
fun addAll(entries: Collection<Entry>) {
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<Entry> = children.toList()
fun findChild(id: String): GenericEntry? = lastOrNull { it.id == id }
@JvmOverloads
fun getChildren(
includeDirs: Boolean = true,
includeFiles: Boolean = true
): List<Entry> {
): List<Child> {
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<Entry> {
return mapNotNull {
it as? Entry
}
}
fun getAlbums(): List<Album> {
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
}
}

View File

@ -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<Artist> = listOf(),
val albums: List<Entry> = listOf(),
val albums: List<Album> = listOf(),
val songs: List<Entry> = listOf()
)

View File

@ -4,6 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
data class SearchTwoResult(
@JsonProperty("artist") val artistList: List<Artist> = emptyList(),
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList(),
@JsonProperty("album") val albumList: List<Album> = emptyList(),
@JsonProperty("song") val songList: List<MusicDirectoryChild> = emptyList()
)

View File

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

View File

@ -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<MusicDirectory.Entry, AlbumRowBinder.ViewHolder>(), KoinComponent {
context: Context
) : ItemViewBinder<MusicDirectory.Album, AlbumRowBinder.ViewHolder>(), 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()

View File

@ -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<ArtistOrIndex, ArtistRowBinder.ViewHolder>(), 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)

View File

@ -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<T : Identifiable> : MultiTypeAdapter() {
// Update the BoundedTreeSet if selection type is changed
@ -34,11 +46,12 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
return getItem(position).longId
}
private fun getItem(position: Int): T {
return mDiffer.currentList[position]
}
// override getIt
override var items: List<Any>
get() = getCurrentList()
set(value) {

View File

@ -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<DividerBinder.Divider, DividerBinder.ViewHolder>() {
class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHolder>() {
// Set our layout files
val layout = R.layout.row_divider
@ -39,7 +37,7 @@ class DividerBinder: ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHol
}
// Class to store our data into
data class Divider(val stringId: Int): Identifiable {
data class Divider(val stringId: Int) : Identifiable {
override val id: String
get() = stringId.toString()
override val longId: Long
@ -47,6 +45,4 @@ class DividerBinder: ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHol
override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId)
}
}
}

View File

@ -21,7 +21,7 @@ class ImageHelper(context: Context) {
var theme: String
fun rebuild(context: Context, force: Boolean = false) {
val currentTheme = Settings.theme!!
val currentTheme = Settings.theme
val themesMatch = theme == currentTheme
if (!themesMatch) theme = currentTheme
@ -31,7 +31,7 @@ class ImageHelper(context: Context) {
}
init {
theme = Settings.theme!!
theme = Settings.theme
getDrawables(context)
}

View File

@ -276,7 +276,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
override fun setChecked(newStatus: Boolean) {
observableChecked.postValue(newStatus)
//check.isChecked = newStatus
// FIXME, check if working
// check.isChecked = newStatus
}
override fun isChecked(): Boolean {

View File

@ -5,9 +5,8 @@ package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Album
fun Album.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry(
fun Album.toDomainEntity(): MusicDirectory.Album = MusicDirectory.Album(
id = this@toDomainEntity.id,
isDirectory = true,
title = this@toDomainEntity.name,
coverArt = this@toDomainEntity.coverArt,
artist = this@toDomainEntity.artist,
@ -24,4 +23,4 @@ fun Album.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().appl
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toDomainEntity() })
}
fun List<Album>.toDomainEntityList(): List<MusicDirectory.Entry> = this.map { it.toDomainEntity() }
fun List<Album>.toDomainEntityList(): List<MusicDirectory.Album> = this.map { it.toDomainEntity() }

View File

@ -23,3 +23,7 @@ fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().
name = this@toMusicDirectoryDomainEntity.name
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
}
fun APIArtist.toDomainEntityList(): List<MusicDirectory.Album> {
return this.albumsList.map { it.toDomainEntity() }
}

View File

@ -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<MusicDirectory.Entry>() {
class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
/**
* The ViewModel to use to get the data
@ -28,16 +28,6 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
*/
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<MusicDirectory.Entry>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Album>> {
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<MusicDirectory.Entry>() {
)
}
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)

View File

@ -27,16 +27,6 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
*/
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<ArtistOrIndex>() {
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)

View File

@ -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<List<MusicDirectory.Entry>> {
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<MusicDirectory.Entry>) {
if (songs.isNotEmpty()) {
val position = songs[0].bookmarkPosition
mediaPlayerController.restore(
songs = songs,
currentPlayingIndex = 0,
currentPlayingPosition = position,
autoPlay = true,
newPlaylist = true
)
}
}
}

View File

@ -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<DownloadFile>() {
/**
@ -60,7 +74,9 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
)
)
viewAdapter.submitList(listModel.getList().value)
val liveDataList = listModel.getList()
viewAdapter.submitList(liveDataList.value)
}
}

View File

@ -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<T : Identifiable> : 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<T : Identifiable> : 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<T : Identifiable> : 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)
}
)

View File

@ -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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), KoinComponent {
)
)
viewAdapter.register(
ArtistRowBinder(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader()
)
)
viewAdapter.register(
DividerBinder()
)
@ -164,7 +178,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), 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<Identifiable>(), 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<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
}
}
@ -454,17 +471,15 @@ class SearchFragment : MultiListFragment<Identifiable>(), 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<Identifiable>(), 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)

View File

@ -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<MusicDirectory.Entry>() {
*/
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<MusicDirectory.Entry>() {
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<MusicDirectory.Entry>() {
listModel.currentListIsSortable = true
}
private fun getSelectedSongs(): List<MusicDirectory.Entry> {
internal fun getSelectedSongs(): List<MusicDirectory.Entry> {
// 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))

View File

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

View File

@ -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<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
val list: MutableLiveData<List<MusicDirectory.Album>> = 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<List<MusicDirectory.Entry>> {
): LiveData<List<MusicDirectory.Album>> {
// 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<MusicDirectory.Entry>()
val list = ArrayList<MusicDirectory.Child>()
list.addAll(this.list.value!!)
list.addAll(musicDirectory.getAllChild())
this.list.postValue(list)
list.addAll(musicDirectory.getChildren())
this.list.postValue(list as List<MusicDirectory.Album>)
} else {
list.postValue(musicDirectory.getAllChild())
list.postValue(musicDirectory.getChildren() as List<MusicDirectory.Album>)
}
loadedUntil = offset

View File

@ -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<MusicDirectory.Entry> = 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())
}
}

View File

@ -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)
}

View File

@ -41,7 +41,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
// Old style TimeLimitedCache
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String, TimeLimitedCache<List<MusicDirectory.Album>>>
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(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<MusicDirectory.Album> {
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 {

View File

@ -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<MusicDirectory.Album>
@Throws(Exception::class)
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory

View File

@ -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<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList()
val albums: MutableList<MusicDirectory.Album> = ArrayList()
val songs: MutableList<MusicDirectory.Entry> = 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<MusicDirectory.Album> {
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<MusicDirectory.Entry>,
albums: MutableList<MusicDirectory.Album>,
songs: MutableList<MusicDirectory.Entry>
) {
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)
}
}
}

View File

@ -143,10 +143,10 @@ open class RESTMusicService(
id: String,
name: String?,
refresh: Boolean
): MusicDirectory {
): List<MusicDirectory.Album> {
val response = API.getArtist(id).execute().throwOnFailure()
return response.body()!!.artist.toMusicDirectoryDomainEntity()
return response.body()!!.artist.toDomainEntityList()
}
@Throws(Exception::class)

View File

@ -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)
}

View File

@ -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) {

View File

@ -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()) {

View File

@ -2,32 +2,8 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/generic_list_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
a:id="@+id/generic_list_recycler"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="2000"
app:fastScrollPopupTextSize="28sp"
app:fastScrollPopupBackgroundSize="42dp"
app:fastScrollPopupBgColor="@color/cyan"
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupPosition="adjacent"
app:fastScrollTrackColor="@color/dividerColor"
app:fastScrollThumbColor="@color/cyan" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/recycler_view" />
</LinearLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:a="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
a:id="@+id/empty_list_text"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:text="@string/select_album.empty"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/swipe_refresh_view"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
a:id="@+id/recycler_view"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="2000"
app:fastScrollPopupTextSize="28sp"
app:fastScrollPopupBackgroundSize="42dp"
app:fastScrollPopupBgColor="@color/cyan"
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupPosition="adjacent"
app:fastScrollTrackColor="@color/dividerColor"
app:fastScrollThumbColor="@color/cyan" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</merge>

View File

@ -1,20 +1,31 @@
<?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="fill_parent">
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical">
<TextView
a:id="@+id/empty_list_text"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:drawablePadding="0dp"
a:gravity="center"
a:padding="12dp"
a:text="@string/search.no_match"
a:textAppearance="?android:attr/textAppearanceMedium"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/search_entries_refresh"
a:id="@+id/swipe_refresh_view"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<androidx.recyclerview.widget.RecyclerView
a:id="@+id/search_list"
<androidx.recyclerview.widget.RecyclerView
a:id="@+id/recycler_view"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0"/>
a:layout_weight="1.0" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -4,51 +4,6 @@
a:layout_width="fill_parent"
a:layout_height="wrap_content">
<TextView
a:id="@+id/search_not_found"
a:text="@string/search.no_match"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:drawablePadding="0dp"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center"
a:padding="12dp"/>
<TextView
a:id="@+id/search_artists"
a:text="@string/search.artists"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:textColor="#EFEFEF"
a:textStyle="bold"
a:background="#ff555555"
a:gravity="center_vertical"
a:paddingStart="4dp"/>
<TextView
a:id="@+id/search_albums"
a:text="@string/search.albums"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:textColor="#EFEFEF"
a:textStyle="bold"
a:background="#ff555555"
a:gravity="center_vertical"
a:paddingStart="4dp"/>
<TextView
a:id="@+id/search_songs"
a:text="@string/search.songs"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:textColor="#EFEFEF"
a:textStyle="bold"
a:background="#ff555555"
a:gravity="center_vertical"
a:paddingStart="4dp"/>
<TextView
a:id="@+id/search_more_artists"

View File

@ -2,7 +2,6 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical" >
<View
@ -10,39 +9,7 @@
a:layout_height="1dp"
a:background="@color/dividerColor" />
<TextView
a:id="@+id/select_album_empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:text="@string/select_album.empty"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/generic_list_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
a:id="@+id/generic_list_recycler"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="2000"
app:fastScrollPopupTextSize="28sp"
app:fastScrollPopupBackgroundSize="42dp"
app:fastScrollPopupBgColor="@color/cyan"
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupPosition="adjacent"
app:fastScrollTrackColor="@color/dividerColor"
app:fastScrollThumbColor="@color/cyan" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/recycler_view" />
<include layout="@layout/album_buttons" />
</LinearLayout>

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}