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.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"?>
|
<?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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue