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.io.Serializable
import java.util.Date import java.util.Date
class MusicDirectory { class MusicDirectory : ArrayList<MusicDirectory.Child>() {
var name: String? = null var name: String? = null
private val children = mutableListOf<Entry>()
fun addAll(entries: Collection<Entry>) { fun addFirst(child: Child) {
children.addAll(entries) add(0, child)
} }
fun addFirst(child: Entry) { fun addChild(child: Child) {
children.add(0, child) add(child)
} }
fun addChild(child: Entry) { fun findChild(id: String): GenericEntry? = lastOrNull { it.id == id }
children.add(child)
}
fun findChild(id: String): Entry? = children.lastOrNull { it.id == id }
fun getAllChild(): List<Entry> = children.toList()
@JvmOverloads @JvmOverloads
fun getChildren( fun getChildren(
includeDirs: Boolean = true, includeDirs: Boolean = true,
includeFiles: Boolean = true includeFiles: Boolean = true
): List<Entry> { ): List<Child> {
if (includeDirs && includeFiles) { 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 @Entity
data class Entry( data class Entry(
@PrimaryKey override var id: String, @PrimaryKey override var id: String,
var parent: String? = null, override var parent: String? = null,
var isDirectory: Boolean = false, override var isDirectory: Boolean = false,
var title: String? = null, override var title: String? = null,
var album: String? = null, override var album: String? = null,
var albumId: String? = null, var albumId: String? = null,
var artist: String? = null, override var artist: String? = null,
var artistId: String? = null, override var artistId: String? = null,
var track: Int? = 0, var track: Int? = null,
var year: Int? = 0, override var year: Int? = null,
var genre: String? = null, override var genre: String? = null,
var contentType: String? = null, var contentType: String? = null,
var suffix: String? = null, var suffix: String? = null,
var transcodedContentType: String? = null, var transcodedContentType: String? = null,
var transcodedSuffix: String? = null, var transcodedSuffix: String? = null,
var coverArt: String? = null, override var coverArt: String? = null,
var size: Long? = null, var size: Long? = null,
var songCount: Long? = null, override var songCount: Long? = null,
var duration: Int? = null, override var duration: Int? = null,
var bitRate: Int? = null, var bitRate: Int? = null,
var path: String? = null, override var path: String? = null,
var isVideo: Boolean = false, var isVideo: Boolean = false,
var starred: Boolean = false, override var starred: Boolean = false,
var discNumber: Int? = null, override var discNumber: Int? = null,
var type: String? = null, var type: String? = null,
var created: Date? = null, override var created: Date? = null,
var closeness: Int = 0, override var closeness: Int = 0,
var bookmarkPosition: Int = 0, var bookmarkPosition: Int = 0,
var userRating: Int? = null, var userRating: Int? = null,
var averageRating: Float? = null var averageRating: Float? = null,
) : Serializable, GenericEntry() { override var name: String? = null
) : Serializable, Child() {
fun setDuration(duration: Long) { fun setDuration(duration: Long) {
this.duration = duration.toInt() this.duration = duration.toInt()
} }
@ -94,4 +122,26 @@ class MusicDirectory {
override fun compareTo(other: Identifiable) = compareTo(other as Entry) 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 package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Album
import org.moire.ultrasonic.domain.MusicDirectory.Entry import org.moire.ultrasonic.domain.MusicDirectory.Entry
/** /**
@ -7,6 +8,6 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry
*/ */
data class SearchResult( data class SearchResult(
val artists: List<Artist> = listOf(), val artists: List<Artist> = listOf(),
val albums: List<Entry> = listOf(), val albums: List<Album> = listOf(),
val songs: List<Entry> = listOf() val songs: List<Entry> = listOf()
) )

View File

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

View File

@ -100,8 +100,8 @@ public class ShufflePlayBuffer
synchronized (buffer) synchronized (buffer)
{ {
buffer.addAll(songs.getChildren()); buffer.addAll(songs.getTracks());
Timber.i("Refilled shuffle play buffer with %d songs.", songs.getChildren().size()); Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size());
} }
} }
catch (Exception x) 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 * Creates a Row in a RecyclerView which contains the details of an Album
*/ */
class AlbumRowBinder( class AlbumRowBinder(
val onItemClick: (MusicDirectory.Entry) -> Unit, val onItemClick: (MusicDirectory.Album) -> Unit,
val onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
context: Context, context: Context
) : ItemViewBinder<MusicDirectory.Entry, AlbumRowBinder.ViewHolder>(), KoinComponent { ) : ItemViewBinder<MusicDirectory.Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
private val starDrawable: Drawable = private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full) Util.getDrawableFromAttribute(context, R.attr.star_full)
@ -46,7 +46,7 @@ class AlbumRowBinder(
val layout = R.layout.album_list_item val layout = R.layout.album_list_item
val contextMenuLayout = R.menu.artist_context_menu 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.album.text = item.title
holder.artist.text = item.artist holder.artist.text = item.artist
holder.details.setOnClickListener { onItemClick(item) } holder.details.setOnClickListener { onItemClick(item) }
@ -86,7 +86,7 @@ class AlbumRowBinder(
/** /**
* Handles the star / unstar action for an album * 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 entry.starred = !entry.starred
star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
val musicService = getMusicService() val musicService = getMusicService()

View File

@ -14,6 +14,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -31,6 +32,7 @@ class ArtistRowBinder(
val onItemClick: (ArtistOrIndex) -> Unit, val onItemClick: (ArtistOrIndex) -> Unit,
val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
private val enableSections: Boolean = true
) : ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(), KoinComponent { ) : ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(), KoinComponent {
val layout = R.layout.artist_list_item val layout = R.layout.artist_list_item
@ -39,6 +41,7 @@ class ArtistRowBinder(
override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) { override fun onBindViewHolder(holder: ViewHolder, item: ArtistOrIndex) {
holder.textView.text = item.name holder.textView.text = item.name
holder.section.text = getSectionForArtist(item) holder.section.text = getSectionForArtist(item)
holder.section.isVisible = enableSections
holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnClickListener { onItemClick(item) }
holder.layout.setOnLongClickListener { holder.layout.setOnLongClickListener {
val popup = Helper.createPopupMenu(holder.itemView) 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 package org.moire.ultrasonic.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -8,10 +15,15 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.drakeet.multitype.MultiTypeAdapter import com.drakeet.multitype.MultiTypeAdapter
import java.util.TreeSet
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.util.BoundedTreeSet 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() { class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
// Update the BoundedTreeSet if selection type is changed // Update the BoundedTreeSet if selection type is changed
@ -34,11 +46,12 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter() {
return getItem(position).longId return getItem(position).longId
} }
private fun getItem(position: Int): T { private fun getItem(position: Int): T {
return mDiffer.currentList[position] return mDiffer.currentList[position]
} }
// override getIt
override var items: List<Any> override var items: List<Any>
get() = getCurrentList() get() = getCurrentList()
set(value) { set(value) {

View File

@ -9,12 +9,10 @@ import com.drakeet.multitype.ItemViewBinder
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
/** /**
* Creates a row in a RecyclerView which can be used as a divide between different sections * 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 // Set our layout files
val layout = R.layout.row_divider val layout = R.layout.row_divider
@ -39,7 +37,7 @@ class DividerBinder: ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHol
} }
// Class to store our data into // Class to store our data into
data class Divider(val stringId: Int): Identifiable { data class Divider(val stringId: Int) : Identifiable {
override val id: String override val id: String
get() = stringId.toString() get() = stringId.toString()
override val longId: Long 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) override fun compareTo(other: Identifiable): Int = longId.compareTo(other.longId)
} }
}
}

View File

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

View File

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

View File

@ -5,9 +5,8 @@ package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Album 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, id = this@toDomainEntity.id,
isDirectory = true,
title = this@toDomainEntity.name, title = this@toDomainEntity.name,
coverArt = this@toDomainEntity.coverArt, coverArt = this@toDomainEntity.coverArt,
artist = this@toDomainEntity.artist, artist = this@toDomainEntity.artist,
@ -24,4 +23,4 @@ fun Album.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().appl
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toDomainEntity() }) 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 name = this@toMusicDirectoryDomainEntity.name
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) 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 * Displays a list of Albums from the media library
* FIXME: Add music folder support * FIXME: Add music folder support
*/ */
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() { class AlbumListFragment : EntryListFragment<MusicDirectory.Album>() {
/** /**
* The ViewModel to use to get the data * 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 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, * The id of the target in the navigation graph where we should go,
* after the user has clicked on an item * 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 * 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") 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)
@ -83,7 +73,7 @@ class AlbumListFragment : EntryListFragment<MusicDirectory.Entry>() {
) )
} }
override fun onItemClick(item: MusicDirectory.Entry) { override fun onItemClick(item: MusicDirectory.Album) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id) bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory) 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 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, * The id of the target in the navigation graph where we should go,
* after the user has clicked on an item * 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_NAME, item.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id) bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist)) bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, bundle.putString(
Constants.ALPHABETICAL_BY_NAME) Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE,
Constants.ALPHABETICAL_BY_NAME
)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, item.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_SIZE, 1000)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) 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 package org.moire.ultrasonic.fragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle 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. * Therefore this fragment allows only for singular selection and playback.
* *
* // FIXME: use restore for playback * FIXME: use restore for playback
*/ */
class BookmarksFragment : TrackCollectionFragment() { class BookmarksFragment : TrackCollectionFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,14 +36,6 @@ class BookmarksFragment : TrackCollectionFragment() {
viewAdapter.selectionType = BaseAdapter.SelectionType.SINGLE 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>> { override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
listModel.viewModelScope.launch(handler) { listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true refreshListView?.isRefreshing = true
@ -47,12 +44,34 @@ class BookmarksFragment : TrackCollectionFragment() {
} }
return listModel.currentList 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 package org.moire.ultrasonic.fragment
import android.app.Application import android.app.Application
@ -14,6 +21,13 @@ import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util 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>() { 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 package org.moire.ultrasonic.fragment
import android.os.Bundle import android.os.Bundle
@ -5,6 +12,8 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -37,6 +46,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
protected var refreshListView: SwipeRefreshLayout? = null protected var refreshListView: SwipeRefreshLayout? = null
internal var listView: RecyclerView? = null internal var listView: RecyclerView? = null
internal lateinit var viewManager: LinearLayoutManager internal lateinit var viewManager: LinearLayoutManager
internal lateinit var emptyTextView: TextView
/** /**
* The Adapter for the RecyclerView * The Adapter for the RecyclerView
@ -76,14 +86,11 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
open val mainLayout: Int = R.layout.generic_list 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 open val refreshListId = R.id.swipe_refresh_view
open val recyclerViewId = R.id.recycler_view
/** open val emptyTextViewId = R.id.empty_list_text
* The id of the RecyclerView
*/
open val recyclerViewId = R.id.generic_list_recycler
open fun setTitle(title: String?) { open fun setTitle(title: String?) {
if (title == null) { if (title == null) {
@ -113,11 +120,15 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
// Populate the LiveData. This starts an API request in most cases // Populate the LiveData. This starts an API request in most cases
liveDataItems = getLiveData(arguments) 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 // Register an observer to update our UI when the data changes
liveDataItems.observe( liveDataItems.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ { newItems ->
newItems -> emptyTextView.isVisible = newItems.isEmpty()
viewAdapter.submitList(newItems) viewAdapter.submitList(newItems)
} }
) )

View File

@ -12,8 +12,8 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.ListAdapter import android.widget.ListAdapter
import android.widget.TextView
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.DividerBinder
import org.moire.ultrasonic.adapters.TrackViewBinder 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 * Initiates a search on the media library and displays the results
*/ */
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent { 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 moreArtistsButton: View? = null
private var moreAlbumsButton: View? = null private var moreAlbumsButton: View? = null
private var moreSongsButton: View? = null private var moreSongsButton: View? = null
@ -71,8 +68,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
override val listModel: SearchListModel by viewModels() override val listModel: SearchListModel by viewModels()
override val recyclerViewId = R.id.search_list
override val mainLayout: Int = R.layout.search override val mainLayout: Int = R.layout.search
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -87,10 +82,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
) )
if (buttons != null) { 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) moreArtistsButton = buttons.findViewById(R.id.search_more_artists)
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums) moreAlbumsButton = buttons.findViewById(R.id.search_more_albums)
moreSongsButton = buttons.findViewById(R.id.search_more_songs) 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 searchRefresh!!.isEnabled = false
// list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long -> // list.setOnItemClickListener(OnItemClickListener { parent: AdapterView<*>, view1: View, position: Int, id: Long ->
@ -132,6 +123,37 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
registerForContextMenu(listView!!) 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( viewAdapter.register(
TrackViewBinder( TrackViewBinder(
checkable = false, 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( viewAdapter.register(
DividerBinder() DividerBinder()
) )
@ -164,7 +178,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
} }
// Fragment was started from the Menu, create empty list // Fragment was started from the Menu, create empty list
populateList(SearchResult()) // populateList(SearchResult())
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -180,11 +194,13 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
val autoPlay = val autoPlay =
arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY) val query = arguments?.getString(Constants.INTENT_EXTRA_NAME_QUERY)
// If started with a query, enter it to the searchView // If started with a query, enter it to the searchView
if (query != null) { if (query != null) {
searchView.setQuery(query, false) searchView.setQuery(query, false)
searchView.clearFocus() searchView.clearFocus()
} }
searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int): Boolean { override fun onSuggestionSelect(position: Int): Boolean {
return true return true
@ -423,8 +439,9 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private fun search(query: String, autoplay: Boolean) { private fun search(query: String, autoplay: Boolean) {
// FIXME support autoplay // FIXME support autoplay
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
refreshListView?.isRefreshing = true
listModel.search(query) listModel.search(query)
refreshListView?.isRefreshing = false
} }
} }
@ -454,17 +471,15 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
} }
val songs = searchResult.songs val songs = searchResult.songs
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_albums)) list.add(DividerBinder.Divider(R.string.search_songs))
list.addAll(songs) list.addAll(songs)
// if (songs.size > DEFAULT_SONGS) { // if (songs.size > DEFAULT_SONGS) {
// moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true) // moreSongsAdapter = mergeAdapter!!.addView(moreSongsButton, true)
// } // }
} }
// FIXME // Show/hide the empty text view
if (list.isEmpty()) { emptyTextView.isVisible = list.isEmpty()
// mergeAdapter!!.addView(notFound, false)
}
viewAdapter.submitList(list) viewAdapter.submitList(list)
} }
@ -506,7 +521,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
// Navigation.findNavController(requireView()).navigate(R.id.searchToSelectAlbum, bundle) // 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() val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id) bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title) bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.title)

View File

@ -30,7 +30,6 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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 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, * The id of the target in the navigation graph where we should go,
* after the user has clicked on an item * after the user has clicked on an item
@ -118,7 +107,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
setupButtons(view) setupButtons(view)
emptyView = view.findViewById(R.id.select_album_empty) emptyView = view.findViewById(R.id.empty_list_text)
registerForContextMenu(listView!!) registerForContextMenu(listView!!)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -629,7 +618,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Entry>() {
listModel.currentListIsSortable = true 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. // Walk through selected set and get the Entries based on the saved ids.
return viewAdapter.getCurrentList().mapNotNull { return viewAdapter.getCurrentList().mapNotNull {
if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId)) if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId))

View File

@ -87,7 +87,7 @@ class ImageLoader(
@JvmOverloads @JvmOverloads
fun loadImage( fun loadImage(
view: View?, view: View?,
entry: MusicDirectory.Entry?, entry: MusicDirectory.Child?,
large: Boolean, large: Boolean,
size: Int, size: Int,
defaultResourceId: Int = R.drawable.unknown_album defaultResourceId: Int = R.drawable.unknown_album

View File

@ -5,7 +5,6 @@ import android.os.Bundle
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
@ -14,7 +13,7 @@ import org.moire.ultrasonic.util.Settings
class AlbumListModel(application: Application) : GenericListModel(application) { 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 var lastType: String? = null
private var loadedUntil: Int = 0 private var loadedUntil: Int = 0
@ -22,7 +21,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
refresh: Boolean, refresh: Boolean,
swipe: SwipeRefreshLayout, swipe: SwipeRefreshLayout,
args: Bundle 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. // Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position // This way, we keep the scroll position
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!! 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?) { fun getAlbumsOfArtist(musicService: MusicService, refresh: Boolean, id: String, name: String?) {
list.postValue(musicService.getArtist(id, name, refresh))
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())
} }
override fun load( override fun load(
@ -108,13 +85,15 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
currentListIsSortable = isCollectionSortable(albumListType) currentListIsSortable = isCollectionSortable(albumListType)
// TODO: Change signature of musicService.getAlbumList to return a List
@Suppress("UNCHECKED_CAST")
if (append && list.value != null) { if (append && list.value != null) {
val list = ArrayList<MusicDirectory.Entry>() val list = ArrayList<MusicDirectory.Child>()
list.addAll(this.list.value!!) list.addAll(this.list.value!!)
list.addAll(musicDirectory.getAllChild()) list.addAll(musicDirectory.getChildren())
this.list.postValue(list) this.list.postValue(list as List<MusicDirectory.Album>)
} else { } else {
list.postValue(musicDirectory.getAllChild()) list.postValue(musicDirectory.getChildren() as List<MusicDirectory.Album>)
} }
loadedUntil = offset loadedUntil = offset

View File

@ -12,7 +12,6 @@ import androidx.lifecycle.MutableLiveData
import java.util.LinkedList import java.util.LinkedList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
@ -54,25 +53,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
} }
} else { } else {
val musicDirectory = service.getMusicDirectory(id, name, refresh) val musicDirectory = service.getMusicDirectory(id, name, refresh)
root = musicDirectory
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
}
} }
currentDirectory.postValue(root) currentDirectory.postValue(root)
@ -87,13 +68,13 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
) { ) {
val service = MusicServiceFactory.getMusicService() val service = MusicServiceFactory.getMusicService()
for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { for (song in parent.getTracks()) {
if (!song.isVideo && !song.isDirectory) { if (!song.isVideo && !song.isDirectory) {
songs.add(song) songs.add(song)
} }
} }
for ((id1, _, _, title) in parent.getChildren(true, includeFiles = false)) { for ((id1, _, _, title) in parent.getAlbums()) {
var root: MusicDirectory var root: MusicDirectory
if (allSongsId != id1) { if (allSongsId != id1) {
@ -118,13 +99,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList() val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
val artist = service.getArtist(parentId, "", false) val artist = service.getArtist(parentId, "", false)
for ((id1) in artist.getChildren()) { // FIXME is still working?
for ((id1) in artist) {
if (allSongsId != id1) { if (allSongsId != id1) {
val albumDirectory = service.getAlbum( val albumDirectory = service.getAlbum(
id1, "", false id1, "", false
) )
for (song in albumDirectory.getChildren()) { for (song in albumDirectory.getTracks()) {
if (!song.isVideo) { if (!song.isVideo) {
songs.add(song) songs.add(song)
} }
@ -252,6 +234,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
} }
private fun updateList(root: MusicDirectory) { 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) { val albums = if (!isOffline && useId3Tags) {
callWithErrorHandling { musicService.getArtist(id, name, false) } callWithErrorHandling { musicService.getArtist(id, name, false) }
} else { } else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) } callWithErrorHandling {
musicService.getMusicDirectory(id, name, false).getAlbums()
}
} }
albums?.getAllChild()?.map { album -> albums?.map { album ->
mediaItems.add( mediaItems.add(
album.title ?: "", album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name) listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
@ -517,7 +519,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all? // 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 -> items.map { item ->
if (item.isDirectory) if (item.isDirectory)
mediaItems.add( mediaItems.add(
@ -573,7 +575,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
} }
albums?.getAllChild()?.map { album -> albums?.getChildren()?.map { album ->
mediaItems.add( mediaItems.add(
album.title ?: "", album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name) 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( mediaItems.add(
R.string.search_more, R.string.search_more,
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), 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) } val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
if (content != null) { if (content != null) {
if (content.getAllChild().count() > 1) if (content.getChildren().count() > 1)
mediaItems.addPlayAllItem( mediaItems.addPlayAllItem(
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|") listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
) )
// Playlist should be cached as it may contain random elements // Playlist should be cached as it may contain random elements
playlistCache = content.getAllChild() playlistCache = content.getTracks()
playlistCache!!.take(DISPLAY_LIMIT).map { item -> playlistCache!!.take(DISPLAY_LIMIT).map { item ->
mediaItems.add( mediaItems.add(
MediaBrowserCompat.MediaItem( MediaBrowserCompat.MediaItem(
@ -657,7 +659,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
if (playlistCache == null) { if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them // This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = callWithErrorHandling { musicService.getPlaylist(id, name) } val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild() playlistCache = content?.getTracks()
} }
if (playlistCache != null) playSongs(playlistCache) if (playlistCache != null) playSongs(playlistCache)
} }
@ -668,7 +670,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
if (playlistCache == null) { if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them // This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = callWithErrorHandling { musicService.getPlaylist(id, name) } val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild() playlistCache = content?.getTracks()
} }
val song = playlistCache?.firstOrNull { x -> x.id == songId } val song = playlistCache?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song) if (song != null) playSong(song)
@ -678,14 +680,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun playAlbum(id: String, name: String) { private fun playAlbum(id: String, name: String) {
serviceScope.launch { serviceScope.launch {
val songs = listSongsInMusicService(id, name) 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) { private fun playAlbumSong(id: String, name: String, songId: String) {
serviceScope.launch { serviceScope.launch {
val songs = listSongsInMusicService(id, name) 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) if (song != null) playSong(song)
} }
} }
@ -717,10 +719,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) { if (episodes != null) {
if (episodes.getAllChild().count() > 1) if (episodes.getTracks().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
episodes.getAllChild().map { episode -> episodes.getTracks().map { episode ->
mediaItems.add( mediaItems.add(
MediaBrowserCompat.MediaItem( MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry( Util.getMediaDescriptionForEntry(
@ -741,7 +743,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
serviceScope.launch { serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) { if (episodes != null) {
playSongs(episodes.getAllChild()) playSongs(episodes.getTracks())
} }
} }
} }
@ -751,7 +753,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) { if (episodes != null) {
val selectedEpisode = episodes val selectedEpisode = episodes
.getAllChild() .getTracks()
.firstOrNull { episode -> episode.id == episodeId } .firstOrNull { episode -> episode.id == episodeId }
if (selectedEpisode != null) playSong(selectedEpisode) if (selectedEpisode != null) playSong(selectedEpisode)
} }
@ -766,7 +768,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
if (bookmarks != null) { if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks) val songs = Util.getSongsFromBookmarks(bookmarks)
songs.getAllChild().map { song -> songs.getTracks().map { song ->
mediaItems.add( mediaItems.add(
MediaBrowserCompat.MediaItem( MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry( Util.getMediaDescriptionForEntry(
@ -787,7 +789,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val bookmarks = callWithErrorHandling { musicService.getBookmarks() } val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) { if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks) 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) if (song != null) playSong(song)
} }
} }
@ -926,11 +928,11 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
if (songs != null) { if (songs != null) {
if (songs.getAllChild().count() > 1) if (songs.getChildren().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all? // TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getAllChild() val items = songs.getTracks()
randomSongsCache = items randomSongsCache = items
items.map { song -> items.map { song ->
mediaItems.add( mediaItems.add(
@ -954,7 +956,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them // 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 // In this case we request a new set of random songs
val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
randomSongsCache = content?.getAllChild() randomSongsCache = content?.getTracks()
} }
if (randomSongsCache != null) playSongs(randomSongsCache) if (randomSongsCache != null) playSongs(randomSongsCache)
} }

View File

@ -41,7 +41,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
// Old style TimeLimitedCache // Old style TimeLimitedCache
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>> 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 cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>> private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS) private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
@ -148,20 +148,21 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { override fun getArtist(id: String, name: String?, refresh: Boolean):
checkSettingsChanged() List<MusicDirectory.Album> {
var cache = if (refresh) null else cachedArtist[id] checkSettingsChanged()
var dir = cache?.get() var cache = if (refresh) null else cachedArtist[id]
if (dir == null) { var dir = cache?.get()
dir = musicService.getArtist(id, name, refresh) if (dir == null) {
cache = TimeLimitedCache( dir = musicService.getArtist(id, name, refresh)
Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS cache = TimeLimitedCache(
) Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS
cache.set(dir) )
cachedArtist.put(id, cache) cache.set(dir)
cachedArtist.put(id, cache)
}
return dir
} }
return dir
}
@Throws(Exception::class) @Throws(Exception::class)
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { 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 import org.moire.ultrasonic.domain.UserInfo
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface MusicService { interface MusicService {
@Throws(Exception::class) @Throws(Exception::class)
fun ping() fun ping()
@ -56,7 +57,7 @@ interface MusicService {
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
@Throws(Exception::class) @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) @Throws(Exception::class)
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory 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.FileWriter
import java.io.InputStream import java.io.InputStream
import java.io.Reader import java.io.Reader
import java.lang.Math.min
import java.util.ArrayList import java.util.ArrayList
import java.util.HashSet import java.util.HashSet
import java.util.LinkedList import java.util.LinkedList
@ -119,7 +118,7 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun search(criteria: SearchCriteria): SearchResult { override fun search(criteria: SearchCriteria): SearchResult {
val artists: MutableList<Artist> = ArrayList() val artists: MutableList<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList() val albums: MutableList<MusicDirectory.Album> = ArrayList()
val songs: MutableList<MusicDirectory.Entry> = ArrayList() val songs: MutableList<MusicDirectory.Entry> = ArrayList()
val root = FileUtil.musicDirectory val root = FileUtil.musicDirectory
var closeness: Int var closeness: Int
@ -258,7 +257,7 @@ class OfflineMusicService : MusicService, KoinComponent {
return result return result
} }
children.shuffle() children.shuffle()
val finalSize: Int = min(children.size, size) val finalSize: Int = children.size.coerceAtMost(size)
for (i in 0 until finalSize) { for (i in 0 until finalSize) {
val file = children[i % children.size] val file = children[i % children.size]
result.addChild(createEntry(file, getName(file))) result.addChild(createEntry(file, getName(file)))
@ -447,9 +446,10 @@ class OfflineMusicService : MusicService, KoinComponent {
} }
@Throws(OfflineException::class) @Throws(OfflineException::class)
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { override fun getArtist(id: String, name: String?, refresh: Boolean):
throw OfflineException("getArtist isn't available in offline mode") List<MusicDirectory.Album> {
} throw OfflineException("getArtist isn't available in offline mode")
}
@Throws(OfflineException::class) @Throws(OfflineException::class)
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
@ -498,7 +498,7 @@ class OfflineMusicService : MusicService, KoinComponent {
} }
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth") @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) val entry = MusicDirectory.Entry(file.path)
entry.isDirectory = file.isDirectory entry.isDirectory = file.isDirectory
entry.parent = file.parent entry.parent = file.parent
@ -600,7 +600,7 @@ class OfflineMusicService : MusicService, KoinComponent {
artistName: String, artistName: String,
file: File, file: File,
criteria: SearchCriteria, criteria: SearchCriteria,
albums: MutableList<MusicDirectory.Entry>, albums: MutableList<MusicDirectory.Album>,
songs: MutableList<MusicDirectory.Entry> songs: MutableList<MusicDirectory.Entry>
) { ) {
var closeness: Int var closeness: Int
@ -611,7 +611,7 @@ class OfflineMusicService : MusicService, KoinComponent {
val album = createEntry(albumFile, albumName) val album = createEntry(albumFile, albumName)
album.artist = artistName album.artist = artistName
album.closeness = closeness album.closeness = closeness
albums.add(album) albums.add(album as MusicDirectory.Album)
} }
for (songFile in FileUtil.listMediaFiles(albumFile)) { for (songFile in FileUtil.listMediaFiles(albumFile)) {
val songName = getName(songFile) val songName = getName(songFile)
@ -622,7 +622,7 @@ class OfflineMusicService : MusicService, KoinComponent {
song.artist = artistName song.artist = artistName
song.album = albumName song.album = albumName
song.closeness = closeness song.closeness = closeness
songs.add(song) songs.add(song as MusicDirectory.Entry)
} }
} }
} else { } else {
@ -632,7 +632,7 @@ class OfflineMusicService : MusicService, KoinComponent {
song.artist = artistName song.artist = artistName
song.album = songName song.album = songName
song.closeness = closeness song.closeness = closeness
songs.add(song) songs.add(song as MusicDirectory.Entry)
} }
} }
} }

View File

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

View File

@ -240,18 +240,13 @@ class DownloadHandler(
if (songs.size > maxSongs) { if (songs.size > maxSongs) {
return return
} }
for (song in parent.getChildren(includeDirs = false, includeFiles = true)) { for (song in parent.getTracks()) {
if (!song.isVideo) { if (!song.isVideo) {
songs.add(song) songs.add(song)
} }
} }
val musicService = getMusicService() val musicService = getMusicService()
for ( for ((id1, _, _, title) in parent.getAlbums()) {
(id1, _, _, title) in parent.getChildren(
includeDirs = true,
includeFiles = false
)
) {
val root: MusicDirectory = if ( val root: MusicDirectory = if (
!isOffline() && !isOffline() &&
Settings.shouldUseId3Tags Settings.shouldUseId3Tags
@ -271,13 +266,13 @@ class DownloadHandler(
} }
val musicService = getMusicService() val musicService = getMusicService()
val artist = musicService.getArtist(id, "", false) val artist = musicService.getArtist(id, "", false)
for ((id1) in artist.getChildren()) { for ((id1) in artist) {
val albumDirectory = musicService.getAlbum( val albumDirectory = musicService.getAlbum(
id1, id1,
"", "",
false false
) )
for (song in albumDirectory.getChildren()) { for (song in albumDirectory.getTracks()) {
if (!song.isVideo) { if (!song.isVideo) {
songs.add(song) 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.ItemTouchHelper.UP
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.BaseAdapter
import timber.log.Timber
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { 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 * @param large Whether to get the key for the large or the default image
* @return String The hash key * @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 if (entry == null) return null
val albumDir = getAlbumDirectory(entry) val albumDir = getAlbumDirectory(entry)
return getAlbumArtKey(albumDir, large) return getAlbumArtKey(albumDir, large)
@ -190,7 +190,7 @@ object FileUtil {
return albumArtDir return albumArtDir
} }
fun getAlbumDirectory(entry: MusicDirectory.Entry): File { fun getAlbumDirectory(entry: MusicDirectory.Child): File {
val dir: File val dir: File
if (!TextUtils.isEmpty(entry.path)) { if (!TextUtils.isEmpty(entry.path)) {
val f = File(fileSystemSafeDir(entry.path)) val f = File(fileSystemSafeDir(entry.path))
@ -457,7 +457,7 @@ object FileUtil {
try { try {
fw.write("#EXTM3U\n") fw.write("#EXTM3U\n")
for (e in playlist.getChildren()) { for (e in playlist.getTracks()) {
var filePath = getSongFile(e).absolutePath var filePath = getSongFile(e).absolutePath
if (!File(filePath).exists()) { if (!File(filePath).exists()) {

View File

@ -2,32 +2,8 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="fill_parent" a:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical"> a:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <include layout="@layout/recycler_view" />
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>
</LinearLayout> </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"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="vertical" a:layout_width="fill_parent"
a:layout_width="fill_parent" a:layout_height="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 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/search_entries_refresh" a:id="@+id/swipe_refresh_view"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="0dip" a:layout_height="0dip"
a:layout_weight="1.0"> a:layout_weight="1.0">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
a:id="@+id/search_list" a:id="@+id/recycler_view"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="0dip" a:layout_height="0dip"
a:layout_weight="1.0"/> a:layout_weight="1.0" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -4,51 +4,6 @@
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content"> 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 <TextView
a:id="@+id/search_more_artists" a:id="@+id/search_more_artists"

View File

@ -2,7 +2,6 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="fill_parent" a:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical" > a:orientation="vertical" >
<View <View
@ -10,39 +9,7 @@
a:layout_height="1dp" a:layout_height="1dp"
a:background="@color/dividerColor" /> a:background="@color/dividerColor" />
<TextView <include layout="@layout/recycler_view" />
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/album_buttons" /> <include layout="@layout/album_buttons" />
</LinearLayout> </LinearLayout>

View File

@ -42,7 +42,7 @@ class APIArtistConverterTest {
with(convertedEntity) { with(convertedEntity) {
name `should be equal to` entity.name 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() .map { it.toDomainEntity() }.toMutableList()
} }
} }

View File

@ -24,8 +24,8 @@ class APIMusicDirectoryConverterTest {
with(convertedEntity) { with(convertedEntity) {
name `should be equal to` entity.name name `should be equal to` entity.name
getAllChild().size `should be equal to` entity.childList.size getChildren().size `should be equal to` entity.childList.size
getAllChild() `should be equal to` entity.childList getChildren() `should be equal to` entity.childList
.map { it.toDomainEntity() }.toMutableList() .map { it.toDomainEntity() }.toMutableList()
} }
} }

View File

@ -26,9 +26,9 @@ class APIPlaylistConverterTest {
with(convertedEntity) { with(convertedEntity) {
name `should be equal to` entity.name name `should be equal to` entity.name
getAllChild().size `should be equal to` entity.entriesList.size getChildren().size `should be equal to` entity.entriesList.size
getAllChild()[0] `should be equal to` entity.entriesList[0].toDomainEntity() getChildren()[0] `should be equal to` entity.entriesList[0].toDomainEntity()
getAllChild()[1] `should be equal to` entity.entriesList[1].toDomainEntity() getChildren()[1] `should be equal to` entity.entriesList[1].toDomainEntity()
} }
} }