mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-16 11:41:16 +01:00
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:
parent
5dfb66eec2
commit
4e37a2483c
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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() }
|
||||
|
@ -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() }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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>
|
||||
|
35
ultrasonic/src/main/res/layout/recycler_view.xml
Normal file
35
ultrasonic/src/main/res/layout/recycler_view.xml
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user