music: refactor model usage
Refactor the way music models are constructed to achieve the following: - Add a unified interface for resolving display names of artists - Disambiguate the role of Header in the music objects - Eliminate the need to load strings in with a context when creating Header instances
This commit is contained in:
parent
51ba72d861
commit
df49e2765f
45 changed files with 315 additions and 245 deletions
|
@ -71,7 +71,10 @@ fun ImageView.bindGenreImage(genre: Genre?) {
|
||||||
load(genre, R.drawable.ic_genre, MosaicFetcher(context))
|
load(genre, R.drawable.ic_genre, MosaicFetcher(context))
|
||||||
|
|
||||||
if (genre != null) {
|
if (genre != null) {
|
||||||
contentDescription = context.getString(R.string.desc_genre_image, genre.name)
|
contentDescription = context.getString(
|
||||||
|
R.string.desc_genre_image,
|
||||||
|
genre.resolvedName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
detailModel.setAlbum(args.albumId, requireContext())
|
detailModel.setAlbum(args.albumId)
|
||||||
|
|
||||||
val detailAdapter = AlbumDetailAdapter(
|
val detailAdapter = AlbumDetailAdapter(
|
||||||
playbackModel, detailModel,
|
playbackModel, detailModel,
|
||||||
|
|
|
@ -48,7 +48,7 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
detailModel.setArtist(args.artistId, requireContext())
|
detailModel.setArtist(args.artistId)
|
||||||
|
|
||||||
val detailAdapter = ArtistDetailAdapter(
|
val detailAdapter = ArtistDetailAdapter(
|
||||||
playbackModel,
|
playbackModel,
|
||||||
|
|
|
@ -74,7 +74,7 @@ abstract class DetailFragment : Fragment() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
|
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
|
||||||
detailModel.finishShowMenu(null, requireContext())
|
detailModel.finishShowMenu(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,12 +138,12 @@ abstract class DetailFragment : Fragment() {
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!, config.anchor.context)
|
detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnDismissListener {
|
setOnDismissListener {
|
||||||
detailModel.finishShowMenu(null, config.anchor.context)
|
detailModel.finishShowMenu(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showItem != null) {
|
if (showItem != null) {
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -30,6 +29,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.HeaderString
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -80,50 +80,50 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
fun setGenre(id: Long, context: Context) {
|
fun setGenre(id: Long) {
|
||||||
if (mCurGenre.value?.id == id) return
|
if (mCurGenre.value?.id == id) return
|
||||||
|
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurGenre.value = musicStore.genres.find { it.id == id }
|
mCurGenre.value = musicStore.genres.find { it.id == id }
|
||||||
refreshGenreData(context)
|
refreshGenreData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtist(id: Long, context: Context) {
|
fun setArtist(id: Long) {
|
||||||
if (mCurArtist.value?.id == id) return
|
if (mCurArtist.value?.id == id) return
|
||||||
|
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurArtist.value = musicStore.artists.find { it.id == id }
|
mCurArtist.value = musicStore.artists.find { it.id == id }
|
||||||
refreshArtistData(context)
|
refreshArtistData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlbum(id: Long, context: Context) {
|
fun setAlbum(id: Long) {
|
||||||
if (mCurAlbum.value?.id == id) return
|
if (mCurAlbum.value?.id == id) return
|
||||||
|
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
||||||
refreshAlbumData(context)
|
refreshAlbumData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark that the menu process is done with the new [SortMode].
|
* Mark that the menu process is done with the new [SortMode].
|
||||||
* Pass null if there was no change.
|
* Pass null if there was no change.
|
||||||
*/
|
*/
|
||||||
fun finishShowMenu(newMode: SortMode?, context: Context) {
|
fun finishShowMenu(newMode: SortMode?) {
|
||||||
mShowMenu.value = null
|
mShowMenu.value = null
|
||||||
|
|
||||||
if (newMode != null) {
|
if (newMode != null) {
|
||||||
when (currentMenuContext) {
|
when (currentMenuContext) {
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
settingsManager.detailAlbumSort = newMode
|
settingsManager.detailAlbumSort = newMode
|
||||||
refreshAlbumData(context)
|
refreshAlbumData()
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
settingsManager.detailArtistSort = newMode
|
settingsManager.detailArtistSort = newMode
|
||||||
refreshArtistData(context)
|
refreshArtistData()
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
settingsManager.detailGenreSort = newMode
|
settingsManager.detailGenreSort = newMode
|
||||||
refreshGenreData(context)
|
refreshGenreData()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
@ -153,13 +153,13 @@ class DetailViewModel : ViewModel() {
|
||||||
isNavigating = navigating
|
isNavigating = navigating
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshGenreData(context: Context) {
|
private fun refreshGenreData() {
|
||||||
val data = mutableListOf<BaseModel>(curGenre.value!!)
|
val data = mutableListOf<BaseModel>(curGenre.value!!)
|
||||||
|
|
||||||
data.add(
|
data.add(
|
||||||
ActionHeader(
|
ActionHeader(
|
||||||
id = -2,
|
id = -2,
|
||||||
name = context.getString(R.string.lbl_songs),
|
string = HeaderString.Single(R.string.lbl_songs),
|
||||||
icon = R.drawable.ic_sort,
|
icon = R.drawable.ic_sort,
|
||||||
desc = R.string.lbl_sort,
|
desc = R.string.lbl_sort,
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
|
@ -174,14 +174,14 @@ class DetailViewModel : ViewModel() {
|
||||||
mGenreData.value = data
|
mGenreData.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArtistData(context: Context) {
|
private fun refreshArtistData() {
|
||||||
val artist = curArtist.value!!
|
val artist = curArtist.value!!
|
||||||
val data = mutableListOf<BaseModel>(artist)
|
val data = mutableListOf<BaseModel>(artist)
|
||||||
|
|
||||||
data.add(
|
data.add(
|
||||||
Header(
|
Header(
|
||||||
id = -2,
|
id = -2,
|
||||||
name = context.getString(R.string.lbl_albums)
|
string = HeaderString.Single(R.string.lbl_albums)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ class DetailViewModel : ViewModel() {
|
||||||
data.add(
|
data.add(
|
||||||
ActionHeader(
|
ActionHeader(
|
||||||
id = -3,
|
id = -3,
|
||||||
name = context.getString(R.string.lbl_songs),
|
string = HeaderString.Single(R.string.lbl_songs),
|
||||||
icon = R.drawable.ic_sort,
|
icon = R.drawable.ic_sort,
|
||||||
desc = R.string.lbl_sort,
|
desc = R.string.lbl_sort,
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
|
@ -205,13 +205,13 @@ class DetailViewModel : ViewModel() {
|
||||||
mArtistData.value = data.toList()
|
mArtistData.value = data.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAlbumData(context: Context) {
|
private fun refreshAlbumData() {
|
||||||
val data = mutableListOf<BaseModel>(curAlbum.value!!)
|
val data = mutableListOf<BaseModel>(curAlbum.value!!)
|
||||||
|
|
||||||
data.add(
|
data.add(
|
||||||
ActionHeader(
|
ActionHeader(
|
||||||
id = -2,
|
id = -2,
|
||||||
name = context.getString(R.string.lbl_songs),
|
string = HeaderString.Single(R.string.lbl_songs),
|
||||||
icon = R.drawable.ic_sort,
|
icon = R.drawable.ic_sort,
|
||||||
desc = R.string.lbl_sort,
|
desc = R.string.lbl_sort,
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
|
|
|
@ -48,7 +48,7 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
detailModel.setGenre(args.genreId, requireContext())
|
detailModel.setGenre(args.genreId)
|
||||||
|
|
||||||
val detailAdapter = GenreDetailAdapter(
|
val detailAdapter = GenreDetailAdapter(
|
||||||
playbackModel,
|
playbackModel,
|
||||||
|
|
|
@ -145,7 +145,7 @@ class AlbumDetailAdapter(
|
||||||
binding.detailName.text = data.name
|
binding.detailName.text = data.name
|
||||||
|
|
||||||
binding.detailSubhead.apply {
|
binding.detailSubhead.apply {
|
||||||
text = data.artist.name
|
text = data.artist.resolvedName
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
detailModel.navToItem(data.artist)
|
detailModel.navToItem(data.artist)
|
||||||
|
|
|
@ -190,10 +190,13 @@ class ArtistDetailAdapter(
|
||||||
|
|
||||||
binding.detailCover.apply {
|
binding.detailCover.apply {
|
||||||
bindArtistImage(data)
|
bindArtistImage(data)
|
||||||
contentDescription = context.getString(R.string.desc_artist_image, data.name)
|
contentDescription = context.getString(
|
||||||
|
R.string.desc_artist_image,
|
||||||
|
data.resolvedName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailName.text = data.name
|
binding.detailName.text = data.resolvedName
|
||||||
|
|
||||||
binding.detailSubhead.text = data.genre?.resolvedName
|
binding.detailSubhead.text = data.genre?.resolvedName
|
||||||
?: context.getString(R.string.def_genre)
|
?: context.getString(R.string.def_genre)
|
||||||
|
|
|
@ -136,10 +136,13 @@ class GenreDetailAdapter(
|
||||||
|
|
||||||
binding.detailCover.apply {
|
binding.detailCover.apply {
|
||||||
bindGenreImage(data)
|
bindGenreImage(data)
|
||||||
contentDescription = context.getString(R.string.desc_artist_image, data.name)
|
contentDescription = context.getString(
|
||||||
|
R.string.desc_genre_image,
|
||||||
|
data.resolvedName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailName.text = data.name
|
binding.detailName.text = data.resolvedName
|
||||||
|
|
||||||
binding.detailSubhead.apply {
|
binding.detailSubhead.apply {
|
||||||
text = context.getPlural(R.plurals.fmt_song_count, data.songs.size)
|
text = context.getPlural(R.plurals.fmt_song_count, data.songs.size)
|
||||||
|
|
|
@ -112,11 +112,11 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
settingsManager.libArtistSort = sort
|
settingsManager.libArtistSort = sort
|
||||||
mArtists.value = sort.sortModels(mArtists.value!!)
|
mArtists.value = sort.sortParents(mArtists.value!!)
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
settingsManager.libGenreSort = sort
|
settingsManager.libGenreSort = sort
|
||||||
mGenres.value = sort.sortModels(mGenres.value!!)
|
mGenres.value = sort.sortParents(mGenres.value!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,8 +139,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal
|
||||||
override fun onLoaded(musicStore: MusicStore) {
|
override fun onLoaded(musicStore: MusicStore) {
|
||||||
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
||||||
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
||||||
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
|
mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists)
|
||||||
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
|
mGenres.value = settingsManager.libGenreSort.sortParents(musicStore.genres)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -62,7 +62,7 @@ class AlbumListFragment : HomeListFragment() {
|
||||||
SortMode.ASCENDING, SortMode.DESCENDING -> album.name.sliceArticle()
|
SortMode.ASCENDING, SortMode.DESCENDING -> album.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.ARTIST -> album.artist.name.sliceArticle()
|
SortMode.ARTIST -> album.artist.resolvedName.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.YEAR -> album.year.toString()
|
SortMode.YEAR -> album.year.toString()
|
||||||
|
|
|
@ -54,7 +54,8 @@ class ArtistListFragment : HomeListFragment() {
|
||||||
|
|
||||||
override val popupProvider: (Int) -> String
|
override val popupProvider: (Int) -> String
|
||||||
get() = { idx ->
|
get() = { idx ->
|
||||||
homeModel.artists.value!![idx].name.sliceArticle().first().uppercase()
|
homeModel.artists.value!![idx].resolvedName
|
||||||
|
.sliceArticle().first().uppercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistAdapter(
|
class ArtistAdapter(
|
||||||
|
|
|
@ -54,7 +54,8 @@ class GenreListFragment : HomeListFragment() {
|
||||||
|
|
||||||
override val popupProvider: (Int) -> String
|
override val popupProvider: (Int) -> String
|
||||||
get() = { idx ->
|
get() = { idx ->
|
||||||
homeModel.genres.value!![idx].name.sliceArticle().first().uppercase()
|
homeModel.genres.value!![idx].resolvedName
|
||||||
|
.sliceArticle().first().uppercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreAdapter(
|
class GenreAdapter(
|
||||||
|
|
|
@ -58,8 +58,9 @@ class SongListFragment : HomeListFragment() {
|
||||||
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
|
SortMode.ARTIST ->
|
||||||
.first().uppercase()
|
song.album.artist.resolvedName
|
||||||
|
.sliceArticle().first().uppercase()
|
||||||
|
|
||||||
SortMode.ALBUM -> song.album.name.sliceArticle()
|
SortMode.ALBUM -> song.album.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
@ -27,46 +28,22 @@ import androidx.annotation.StringRes
|
||||||
/**
|
/**
|
||||||
* The base data object for all music.
|
* The base data object for all music.
|
||||||
* @property id The ID that is assigned to this object
|
* @property id The ID that is assigned to this object
|
||||||
* @property name The name of this object (Such as a song title)
|
|
||||||
*/
|
*/
|
||||||
sealed class BaseModel {
|
sealed class BaseModel {
|
||||||
abstract val id: Long
|
abstract val id: Long
|
||||||
abstract val name: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
sealed class Music : BaseModel() {
|
||||||
* Provides a versatile static hash for a music item that will not change when
|
abstract val name: String
|
||||||
* MediaStore changes.
|
abstract val hash: Int
|
||||||
*
|
|
||||||
* The reason why this is used is down a couple of reasons:
|
|
||||||
* - MediaStore will refresh the unique ID of a piece of media whenever the library
|
|
||||||
* changes, which creates bad UX
|
|
||||||
* - Using song names makes collisions too common to be reliable
|
|
||||||
* - Hashing into an integer makes databases both smaller and more efficent
|
|
||||||
*
|
|
||||||
* This does lock me into a "Load everything at once, lol" architecture for Auxio, but I
|
|
||||||
* think its worth it.
|
|
||||||
*
|
|
||||||
* @property hash A unique-ish hash for this media item
|
|
||||||
*
|
|
||||||
* TODO: Make this hash stronger
|
|
||||||
*/
|
|
||||||
sealed interface Hashable {
|
|
||||||
val hash: Int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [BaseModel] variant that denotes that this object is a parent of other data objects, such
|
* [BaseModel] variant that denotes that this object is a parent of other data objects, such
|
||||||
* as an [Album] or [Artist]
|
* as an [Album] or [Artist]
|
||||||
* @property displayName Name that handles the usage of [Genre.resolvedName]
|
|
||||||
* and the normal [BaseModel.name]
|
|
||||||
*/
|
*/
|
||||||
sealed class Parent : BaseModel(), Hashable {
|
sealed class Parent : Music() {
|
||||||
val displayName: String get() = if (this is Genre) {
|
abstract val resolvedName: String
|
||||||
resolvedName
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,7 +69,7 @@ data class Song(
|
||||||
val year: Int,
|
val year: Int,
|
||||||
val track: Int,
|
val track: Int,
|
||||||
val duration: Long
|
val duration: Long
|
||||||
) : BaseModel(), Hashable {
|
) : Music() {
|
||||||
private var mAlbum: Album? = null
|
private var mAlbum: Album? = null
|
||||||
private var mGenre: Genre? = null
|
private var mGenre: Genre? = null
|
||||||
|
|
||||||
|
@ -145,6 +122,10 @@ data class Album(
|
||||||
val totalDuration: String get() =
|
val totalDuration: String get() =
|
||||||
songs.sumOf { it.seconds }.toDuration()
|
songs.sumOf { it.seconds }.toDuration()
|
||||||
|
|
||||||
|
fun linkArtist(artist: Artist) {
|
||||||
|
mArtist = artist
|
||||||
|
}
|
||||||
|
|
||||||
override val hash: Int get() {
|
override val hash: Int get() {
|
||||||
var result = name.hashCode()
|
var result = name.hashCode()
|
||||||
result = 31 * result + artistName.hashCode()
|
result = 31 * result + artistName.hashCode()
|
||||||
|
@ -152,9 +133,8 @@ data class Album(
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun linkArtist(artist: Artist) {
|
override val resolvedName: String
|
||||||
mArtist = artist
|
get() = name
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,6 +146,7 @@ data class Album(
|
||||||
data class Artist(
|
data class Artist(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
override val resolvedName: String,
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
init {
|
init {
|
||||||
|
@ -190,44 +171,114 @@ data class Artist(
|
||||||
/**
|
/**
|
||||||
* The data object for a genre. Inherits [Parent]
|
* The data object for a genre. Inherits [Parent]
|
||||||
* @property songs The list of all [Song]s in this genre.
|
* @property songs The list of all [Song]s in this genre.
|
||||||
* @property resolvedName A name that has been resolved from its int-genre form to its named form.
|
|
||||||
*/
|
*/
|
||||||
data class Genre(
|
data class Genre(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
override val resolvedName: String
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
private val mSongs = mutableListOf<Song>()
|
private val mSongs = mutableListOf<Song>()
|
||||||
val songs: List<Song> get() = mSongs
|
val songs: List<Song> get() = mSongs
|
||||||
|
|
||||||
val resolvedName =
|
|
||||||
name.getGenreNameCompat() ?: name
|
|
||||||
|
|
||||||
val totalDuration: String get() =
|
val totalDuration: String get() =
|
||||||
songs.sumOf { it.seconds }.toDuration()
|
songs.sumOf { it.seconds }.toDuration()
|
||||||
|
|
||||||
override val hash = name.hashCode()
|
|
||||||
|
|
||||||
fun linkSong(song: Song) {
|
fun linkSong(song: Song) {
|
||||||
mSongs.add(song)
|
mSongs.add(song)
|
||||||
song.linkGenre(this)
|
song.linkGenre(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val hash = name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string used for a header instance. This class is a bit complex, mostly because it revolves
|
||||||
|
* around passing string resources that are then resolved by the view instead of passing a context
|
||||||
|
* directly.
|
||||||
|
*/
|
||||||
|
sealed class HeaderString {
|
||||||
|
/** A single string resource. */
|
||||||
|
class Single(@StringRes val id: Int) : HeaderString()
|
||||||
|
/** A string resource with an argument. */
|
||||||
|
class WithArg(@StringRes val id: Int, val arg: Arg) : HeaderString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve this instance into a string.
|
||||||
|
*/
|
||||||
|
fun resolve(context: Context): String {
|
||||||
|
return when (this) {
|
||||||
|
is Single -> context.getString(id)
|
||||||
|
is WithArg -> context.getString(id, arg.resolve(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return false
|
||||||
|
|
||||||
|
return when (this) {
|
||||||
|
is Single -> other is Single && other.id == id
|
||||||
|
is WithArg -> other is WithArg && other.id == id && other.arg == arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return when (this) {
|
||||||
|
is Single -> id.hashCode()
|
||||||
|
is WithArg -> 31 * id.hashCode() * arg.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument for the [WithArg] header string.
|
||||||
|
*/
|
||||||
|
sealed class Arg {
|
||||||
|
/** A string resource to be used as the argument */
|
||||||
|
class Resource(@StringRes val id: Int) : Arg()
|
||||||
|
/** A string value to be used as the argument */
|
||||||
|
class Value(val string: String) : Arg()
|
||||||
|
|
||||||
|
/** Resolve this argument instance into a string. */
|
||||||
|
fun resolve(context: Context): String {
|
||||||
|
return when (this) {
|
||||||
|
is Resource -> context.getString(id)
|
||||||
|
is Value -> string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return false
|
||||||
|
|
||||||
|
return when (this) {
|
||||||
|
is Resource -> other is Resource && other.id == id
|
||||||
|
is Value -> other is Value && other.string == this.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return when (this) {
|
||||||
|
is Resource -> id.hashCode()
|
||||||
|
is Value -> 31 * string.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data object used solely for the "Header" UI element.
|
* A data object used solely for the "Header" UI element.
|
||||||
|
* @see HeaderString
|
||||||
*/
|
*/
|
||||||
data class Header(
|
data class Header(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
val string: HeaderString
|
||||||
) : BaseModel()
|
) : BaseModel()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data object used for an action header. Like [Header], but with a button.
|
* A data object used for an action header. Like [Header], but with a button.
|
||||||
* Inherits [BaseModel].
|
* @see HeaderString
|
||||||
*/
|
*/
|
||||||
data class ActionHeader(
|
data class ActionHeader(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
val string: HeaderString,
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int,
|
||||||
@StringRes val desc: Int,
|
@StringRes val desc: Int,
|
||||||
val onClick: (View) -> Unit,
|
val onClick: (View) -> Unit,
|
||||||
|
@ -239,7 +290,7 @@ data class ActionHeader(
|
||||||
if (other !is ActionHeader) return false
|
if (other !is ActionHeader) return false
|
||||||
|
|
||||||
if (id != other.id) return false
|
if (id != other.id) return false
|
||||||
if (name != other.name) return false
|
if (string != other.string) return false
|
||||||
if (icon != other.icon) return false
|
if (icon != other.icon) return false
|
||||||
if (desc != other.desc) return false
|
if (desc != other.desc) return false
|
||||||
|
|
||||||
|
@ -248,7 +299,7 @@ data class ActionHeader(
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = id.hashCode()
|
var result = id.hashCode()
|
||||||
result = 31 * result + name.hashCode()
|
result = 31 * result + string.hashCode()
|
||||||
result = 31 * result + icon
|
result = 31 * result + icon
|
||||||
result = 31 * result + desc
|
result = 31 * result + desc
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Audio.Genres
|
import android.provider.MediaStore.Audio.Genres
|
||||||
import android.provider.MediaStore.Audio.Media
|
import android.provider.MediaStore.Audio.Media
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
@ -152,7 +153,7 @@ class MusicLoader(private val context: Context) {
|
||||||
// No non-broken genre would be missing a name.
|
// No non-broken genre would be missing a name.
|
||||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||||
|
|
||||||
genres.add(Genre(id, name))
|
genres.add(Genre(id, name, name.getGenreNameCompat() ?: name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,18 +262,25 @@ class MusicLoader(private val context: Context) {
|
||||||
val albumsByArtist = albums.groupBy { it.artistName }
|
val albumsByArtist = albums.groupBy { it.artistName }
|
||||||
|
|
||||||
albumsByArtist.forEach { entry ->
|
albumsByArtist.forEach { entry ->
|
||||||
|
val resolvedName = if (entry.key == MediaStore.UNKNOWN_STRING) {
|
||||||
|
context.getString(R.string.def_artist)
|
||||||
|
} else {
|
||||||
|
entry.key
|
||||||
|
}
|
||||||
|
|
||||||
// Because of our hacky album artist system, MediaStore artist IDs are unreliable.
|
// Because of our hacky album artist system, MediaStore artist IDs are unreliable.
|
||||||
// Therefore we just use the hashCode of the artist name as our ID and move on.
|
// Therefore we just use the hashCode of the artist name as our ID and move on.
|
||||||
artists.add(
|
artists.add(
|
||||||
Artist(
|
Artist(
|
||||||
id = entry.key.hashCode().toLong(),
|
id = entry.key.hashCode().toLong(),
|
||||||
name = entry.key,
|
name = entry.key,
|
||||||
|
resolvedName = resolvedName,
|
||||||
albums = entry.value
|
albums = entry.value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
artists = SortMode.ASCENDING.sortModels(artists).toMutableList()
|
artists = SortMode.ASCENDING.sortParents(artists).toMutableList()
|
||||||
|
|
||||||
logD("Albums successfully linked into ${artists.size} artists")
|
logD("Albums successfully linked into ${artists.size} artists")
|
||||||
}
|
}
|
||||||
|
@ -304,7 +312,8 @@ class MusicLoader(private val context: Context) {
|
||||||
|
|
||||||
val unknownGenre = Genre(
|
val unknownGenre = Genre(
|
||||||
id = Long.MIN_VALUE,
|
id = Long.MIN_VALUE,
|
||||||
name = context.getString(R.string.def_genre)
|
name = MediaStore.UNKNOWN_STRING,
|
||||||
|
resolvedName = context.getString(R.string.def_genre)
|
||||||
)
|
)
|
||||||
|
|
||||||
songs.forEach { song ->
|
songs.forEach { song ->
|
||||||
|
|
|
@ -122,11 +122,7 @@ class PlaybackFragment : Fragment() {
|
||||||
binding.playbackSeekBar.setProgress(pos)
|
binding.playbackSeekBar.setProgress(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
|
playbackModel.displayQueue.observe(viewLifecycleOwner) {
|
||||||
updateQueueIcon(queueItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
|
||||||
updateQueueIcon(queueItem)
|
updateQueueIcon(queueItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,20 @@ package org.oxycblt.auxio.playback
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.ActionHeader
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.HeaderString
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Parent
|
import org.oxycblt.auxio.music.Parent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -89,16 +95,72 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
/** The current repeat mode, see [LoopMode] for more information */
|
/** The current repeat mode, see [LoopMode] for more information */
|
||||||
val loopMode: LiveData<LoopMode> get() = mLoopMode
|
val loopMode: LiveData<LoopMode> get() = mLoopMode
|
||||||
|
|
||||||
|
/** The queue, without the previous items. */
|
||||||
|
val nextItemsInQueue = Transformations.map(queue) { queue ->
|
||||||
|
queue.slice((mIndex.value!! + 1) until queue.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The combined queue data used for UIs, with header data included */
|
||||||
|
val displayQueue = MediatorLiveData<List<BaseModel>>().apply {
|
||||||
|
val combine: (userQueue: List<Song>, nextQueue: List<Song>) -> List<BaseModel> =
|
||||||
|
{ userQueue, nextQueue ->
|
||||||
|
val queue = mutableListOf<BaseModel>()
|
||||||
|
|
||||||
|
if (userQueue.isNotEmpty()) {
|
||||||
|
queue += ActionHeader(
|
||||||
|
id = -2,
|
||||||
|
string = HeaderString.Single(R.string.lbl_next_user_queue),
|
||||||
|
icon = R.drawable.ic_clear,
|
||||||
|
desc = R.string.desc_clear_user_queue,
|
||||||
|
onClick = { playbackManager.clearUserQueue() }
|
||||||
|
)
|
||||||
|
|
||||||
|
queue += userQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextQueue.isNotEmpty()) {
|
||||||
|
val parentName = parent.value?.name
|
||||||
|
|
||||||
|
queue += Header(
|
||||||
|
id = -3,
|
||||||
|
string = HeaderString.WithArg(
|
||||||
|
R.string.fmt_next_from,
|
||||||
|
if (parentName != null) {
|
||||||
|
HeaderString.Arg.Value(parentName)
|
||||||
|
} else {
|
||||||
|
HeaderString.Arg.Resource(R.string.lbl_all_songs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
queue += nextQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
queue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not move these around. The transformed value must be generated through this
|
||||||
|
// observer call first before the userQueue source uses it assuming that it's not
|
||||||
|
// null.
|
||||||
|
addSource(nextItemsInQueue) { nextQueue ->
|
||||||
|
value = combine(userQueue.value!!, nextQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSource(userQueue) { userQueue ->
|
||||||
|
value = combine(
|
||||||
|
userQueue,
|
||||||
|
requireNotNull(nextItemsInQueue.value) {
|
||||||
|
"Transformed value was not generated yet."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** The position as SeekBar progress. */
|
/** The position as SeekBar progress. */
|
||||||
val positionAsProgress = Transformations.map(mPosition) {
|
val positionAsProgress = Transformations.map(mPosition) {
|
||||||
if (mSong.value != null) it.toInt() else 0
|
if (mSong.value != null) it.toInt() else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The queue, without the previous items. */
|
|
||||||
val nextItemsInQueue = Transformations.map(mQueue) {
|
|
||||||
it.slice((mIndex.value!! + 1) until it.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
private val playbackManager = PlaybackStateManager.maybeGetInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
|
@ -316,13 +378,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.addToUserQueue(settingsManager.detailAlbumSort.sortAlbum(album))
|
playbackManager.addToUserQueue(settingsManager.detailAlbumSort.sortAlbum(album))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the user queue entirely
|
|
||||||
*/
|
|
||||||
fun clearUserQueue() {
|
|
||||||
playbackManager.clearUserQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -115,8 +115,6 @@ class QueueAdapter(
|
||||||
fun removeItem(adapterIndex: Int) {
|
fun removeItem(adapterIndex: Int) {
|
||||||
data.removeAt(adapterIndex)
|
data.removeAt(adapterIndex)
|
||||||
|
|
||||||
logD(data)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the data from the next queue is now entirely empty [Signified by a header at the
|
* If the data from the next queue is now entirely empty [Signified by a header at the
|
||||||
* end, remove the next queue header as notify as such.
|
* end, remove the next queue header as notify as such.
|
||||||
|
|
|
@ -27,11 +27,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.music.ActionHeader
|
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
|
||||||
import org.oxycblt.auxio.music.Header
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
|
|
||||||
|
@ -79,22 +75,13 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
playbackModel.userQueue.observe(viewLifecycleOwner) { userQueue ->
|
playbackModel.displayQueue.observe(viewLifecycleOwner) { queue ->
|
||||||
if (userQueue.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAdapter.submitList(createQueueData())
|
queueAdapter.submitList(queue.toMutableList())
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { nextQueue ->
|
|
||||||
if (nextQueue.isEmpty() && playbackModel.userQueue.value!!.isEmpty()) {
|
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAdapter.submitList(createQueueData())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
||||||
|
@ -109,40 +96,4 @@ class QueueFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEUE DATA ---
|
// --- QUEUE DATA ---
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the queue data that should be displayed
|
|
||||||
* @return The list of headers/songs that should be displayed.
|
|
||||||
*/
|
|
||||||
private fun createQueueData(): MutableList<BaseModel> {
|
|
||||||
val queue = mutableListOf<BaseModel>()
|
|
||||||
val userQueue = playbackModel.userQueue.value!!
|
|
||||||
val nextQueue = playbackModel.nextItemsInQueue.value!!
|
|
||||||
|
|
||||||
if (userQueue.isNotEmpty()) {
|
|
||||||
queue += ActionHeader(
|
|
||||||
id = -2,
|
|
||||||
name = getString(R.string.lbl_next_user_queue),
|
|
||||||
icon = R.drawable.ic_clear,
|
|
||||||
desc = R.string.desc_clear_user_queue,
|
|
||||||
onClick = { playbackModel.clearUserQueue() }
|
|
||||||
)
|
|
||||||
|
|
||||||
queue += userQueue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextQueue.isNotEmpty()) {
|
|
||||||
queue += Header(
|
|
||||||
id = -3,
|
|
||||||
name = getString(
|
|
||||||
R.string.fmt_next_from,
|
|
||||||
playbackModel.parent.value?.displayName ?: getString(R.string.lbl_all_songs)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
queue += nextQueue
|
|
||||||
}
|
|
||||||
|
|
||||||
return queue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ class PlaybackNotification private constructor(
|
||||||
*/
|
*/
|
||||||
fun setMetadata(song: Song, onDone: () -> Unit) {
|
fun setMetadata(song: Song, onDone: () -> Unit) {
|
||||||
setContentTitle(song.name)
|
setContentTitle(song.name)
|
||||||
setContentText(song.album.artist.name)
|
setContentText(song.album.artist.resolvedName)
|
||||||
|
|
||||||
// On older versions of android [API <24], show the song's album on the subtext instead of
|
// On older versions of android [API <24], show the song's album on the subtext instead of
|
||||||
// the current mode, as that makes more sense for the old style of media notifications.
|
// the current mode, as that makes more sense for the old style of media notifications.
|
||||||
|
@ -125,7 +125,7 @@ class PlaybackNotification private constructor(
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
||||||
|
|
||||||
// A blank parent always means that the mode is ALL_SONGS
|
// A blank parent always means that the mode is ALL_SONGS
|
||||||
setSubText(parent?.displayName ?: context.getString(R.string.lbl_all_songs))
|
setSubText(parent?.resolvedName ?: context.getString(R.string.lbl_all_songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOTIFICATION ACTION BUILDERS ---
|
// --- NOTIFICATION ACTION BUILDERS ---
|
||||||
|
|
|
@ -114,13 +114,15 @@ class PlaybackSessionConnector(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val artistName = song.album.artist.resolvedName
|
||||||
|
|
||||||
val builder = MediaMetadataCompat.Builder()
|
val builder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.album.artist.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, song.album.artist.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, song.album.artist.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.album.artist.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||||
|
@ -40,8 +41,8 @@ import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchAdapter(
|
class SearchAdapter(
|
||||||
private val doOnClick: (data: BaseModel) -> Unit,
|
private val doOnClick: (data: Music) -> Unit,
|
||||||
private val doOnLongClick: (view: View, data: BaseModel) -> Unit
|
private val doOnLongClick: (view: View, data: Music) -> Unit
|
||||||
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
|
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
|
|
@ -35,9 +35,9 @@ import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -54,10 +54,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by viewModels {
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
SearchViewModel.Factory(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
@ -183,7 +180,7 @@ class SearchFragment : Fragment() {
|
||||||
* Function that handles when an [item] is selected.
|
* Function that handles when an [item] is selected.
|
||||||
* Handles all datatypes that are selectable.
|
* Handles all datatypes that are selectable.
|
||||||
*/
|
*/
|
||||||
private fun onItemSelection(item: BaseModel, imm: InputMethodManager) {
|
private fun onItemSelection(item: Music, imm: InputMethodManager) {
|
||||||
if (item is Song) {
|
if (item is Song) {
|
||||||
playbackModel.playSong(item)
|
playbackModel.playSong(item)
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,17 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.search
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.HeaderString
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -38,7 +38,7 @@ import java.text.Normalizer
|
||||||
* The [ViewModel] for the search functionality
|
* The [ViewModel] for the search functionality
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback {
|
class SearchViewModel : ViewModel(), MusicStore.MusicCallback {
|
||||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||||
private var mIsNavigating = false
|
private var mIsNavigating = false
|
||||||
private var mFilterMode: DisplayMode? = null
|
private var mFilterMode: DisplayMode? = null
|
||||||
|
@ -51,11 +51,6 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val songHeader = Header(id = -1, context.getString(R.string.lbl_songs))
|
|
||||||
private val albumHeader = Header(id = -1, context.getString(R.string.lbl_albums))
|
|
||||||
private val artistHeader = Header(id = -1, context.getString(R.string.lbl_artists))
|
|
||||||
private val genreHeader = Header(id = -1, context.getString(R.string.lbl_genres))
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mFilterMode = settingsManager.searchFilterMode
|
mFilterMode = settingsManager.searchFilterMode
|
||||||
|
|
||||||
|
@ -83,28 +78,28 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
||||||
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
||||||
results.add(artistHeader)
|
results.add(Header(-1, HeaderString.Single(R.string.lbl_artists)))
|
||||||
results.addAll(artists)
|
results.addAll(artists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
||||||
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
||||||
results.add(albumHeader)
|
results.add(Header(-1, HeaderString.Single(R.string.lbl_albums)))
|
||||||
results.addAll(albums)
|
results.addAll(albums)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
||||||
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
||||||
results.add(genreHeader)
|
results.add(Header(-1, HeaderString.Single(R.string.lbl_genres)))
|
||||||
results.addAll(genres)
|
results.addAll(genres)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
||||||
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
||||||
results.add(songHeader)
|
results.add(Header(-1, HeaderString.Single(R.string.lbl_songs)))
|
||||||
results.addAll(songs)
|
results.addAll(songs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,7 +131,7 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback
|
||||||
* Shortcut that will run a ignoreCase filter on a list and only return
|
* Shortcut that will run a ignoreCase filter on a list and only return
|
||||||
* a value if the resulting list is empty.
|
* a value if the resulting list is empty.
|
||||||
*/
|
*/
|
||||||
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
|
private fun List<Music>.filterByOrNull(value: String): List<BaseModel>? {
|
||||||
val filtered = filter {
|
val filtered = filter {
|
||||||
// First see if the normal item name will work. If that fails, try the "normalized"
|
// First see if the normal item name will work. If that fails, try the "normalized"
|
||||||
// [e.g all accented/unicode chars become latin chars] instead. Hopefully this
|
// [e.g all accented/unicode chars become latin chars] instead. Hopefully this
|
||||||
|
@ -195,15 +190,4 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
MusicStore.cancelAwaitInstance(this)
|
MusicStore.cancelAwaitInstance(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
|
||||||
check(modelClass.isAssignableFrom(SearchViewModel::class.java)) {
|
|
||||||
"SearchViewModel.Factory does not support this class"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return SearchViewModel(context) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ import androidx.annotation.IdRes
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Parent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,7 +44,8 @@ enum class SortMode(@IdRes val itemId: Int) {
|
||||||
* Sort a list of songs.
|
* Sort a list of songs.
|
||||||
*
|
*
|
||||||
* **Behavior:**
|
* **Behavior:**
|
||||||
* - [ASCENDING] & [DESCENDING]: See [sortModels]
|
* - [ASCENDING]: By name after article, ascending
|
||||||
|
* - [DESCENDING]: By name after article, descending
|
||||||
* - [ARTIST]: Grouped by album and then sorted [ASCENDING] based off the artist name.
|
* - [ARTIST]: Grouped by album and then sorted [ASCENDING] based off the artist name.
|
||||||
* - [ALBUM]: Grouped by album and sorted [ASCENDING]
|
* - [ALBUM]: Grouped by album and sorted [ASCENDING]
|
||||||
* - [YEAR]: Grouped by album and sorted by year
|
* - [YEAR]: Grouped by album and sorted by year
|
||||||
|
@ -54,7 +55,17 @@ enum class SortMode(@IdRes val itemId: Int) {
|
||||||
*/
|
*/
|
||||||
fun sortSongs(songs: Collection<Song>): List<Song> {
|
fun sortSongs(songs: Collection<Song>): List<Song> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
ASCENDING, DESCENDING -> sortModels(songs)
|
ASCENDING -> songs.sortedWith(
|
||||||
|
compareBy(String.CASE_INSENSITIVE_ORDER) { song ->
|
||||||
|
song.name.sliceArticle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DESCENDING -> songs.sortedWith(
|
||||||
|
compareByDescending(String.CASE_INSENSITIVE_ORDER) { song ->
|
||||||
|
song.name.sliceArticle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
|
else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
|
||||||
ASCENDING.sortAlbum(album)
|
ASCENDING.sortAlbum(album)
|
||||||
|
@ -66,7 +77,8 @@ enum class SortMode(@IdRes val itemId: Int) {
|
||||||
* Sort a list of albums.
|
* Sort a list of albums.
|
||||||
*
|
*
|
||||||
* **Behavior:**
|
* **Behavior:**
|
||||||
* - [ASCENDING] & [DESCENDING]: See [sortModels]
|
* - [ASCENDING]: By name after article, ascending
|
||||||
|
* - [DESCENDING]: By name after article, descending
|
||||||
* - [ARTIST]: Grouped by artist and sorted [ASCENDING]
|
* - [ARTIST]: Grouped by artist and sorted [ASCENDING]
|
||||||
* - [ALBUM]: [ASCENDING]
|
* - [ALBUM]: [ASCENDING]
|
||||||
* - [YEAR]: Sorted by year
|
* - [YEAR]: Sorted by year
|
||||||
|
@ -75,43 +87,40 @@ enum class SortMode(@IdRes val itemId: Int) {
|
||||||
*/
|
*/
|
||||||
fun sortAlbums(albums: Collection<Album>): List<Album> {
|
fun sortAlbums(albums: Collection<Album>): List<Album> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
ASCENDING, DESCENDING -> sortModels(albums)
|
ASCENDING, DESCENDING -> sortParents(albums)
|
||||||
|
|
||||||
ARTIST -> ASCENDING.sortModels(albums.groupBy { it.artist }.keys)
|
ARTIST -> ASCENDING.sortParents(albums.groupBy { it.artist }.keys)
|
||||||
.flatMap { YEAR.sortAlbums(it.albums) }
|
.flatMap { YEAR.sortAlbums(it.albums) }
|
||||||
|
|
||||||
ALBUM -> ASCENDING.sortModels(albums)
|
ALBUM -> ASCENDING.sortParents(albums)
|
||||||
|
|
||||||
YEAR -> albums.sortedByDescending { it.year }
|
YEAR -> albums.sortedByDescending { it.year }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a list of generic [BaseModel] instances.
|
* Sort a generic list of [Parent] instances.
|
||||||
*
|
*
|
||||||
* **Behavior:**
|
* **Behavior:**
|
||||||
* - [ASCENDING]: Sorted by name, ascending
|
* - [ASCENDING]: By name after article, ascending
|
||||||
* - [DESCENDING]: Sorted by name, descending
|
* - [DESCENDING]: By name after article, descending
|
||||||
* - Same list is returned otherwise.
|
* - Same parent list is returned otherwise.
|
||||||
*
|
|
||||||
* Names will be treated as case-insensitive. Articles like "the" and "a" will be skipped
|
|
||||||
* to line up with MediaStore behavior.
|
|
||||||
*/
|
*/
|
||||||
fun <T : BaseModel> sortModels(models: Collection<T>): List<T> {
|
fun <T : Parent> sortParents(parents: Collection<T>): List<T> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
ASCENDING -> models.sortedWith(
|
ASCENDING -> parents.sortedWith(
|
||||||
compareBy(String.CASE_INSENSITIVE_ORDER) { model ->
|
compareBy(String.CASE_INSENSITIVE_ORDER) { model ->
|
||||||
model.name.sliceArticle()
|
model.resolvedName.sliceArticle()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
DESCENDING -> models.sortedWith(
|
DESCENDING -> parents.sortedWith(
|
||||||
compareByDescending(String.CASE_INSENSITIVE_ORDER) { model ->
|
compareByDescending(String.CASE_INSENSITIVE_ORDER) { model ->
|
||||||
model.name.sliceArticle()
|
model.resolvedName.sliceArticle()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> models.toList()
|
else -> parents.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
android:layout_marginStart="@dimen/spacing_mid_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
||||||
android:text="@{song.album.artist.name}"
|
android:text="@{song.album.artist.resolvedName}"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
android:layout_marginStart="@dimen/spacing_mid_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
||||||
android:text="@{song.album.artist.name}"
|
android:text="@{song.album.artist.resolvedName}"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
android:layout_marginStart="@dimen/spacing_mid_huge"
|
android:layout_marginStart="@dimen/spacing_mid_huge"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_huge"
|
android:layout_marginEnd="@dimen/spacing_mid_huge"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
||||||
android:text="@{song.album.artist.name}"
|
android:text="@{song.album.artist.resolvedName}"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
android:layout_marginStart="@dimen/spacing_small"
|
android:layout_marginStart="@dimen/spacing_small"
|
||||||
android:layout_marginEnd="@dimen/spacing_small"
|
android:layout_marginEnd="@dimen/spacing_small"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
|
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
android:layout_marginStart="@dimen/spacing_mid_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
||||||
android:text="@{song.album.artist.name}"
|
android:text="@{song.album.artist.resolvedName}"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
style="@style/Widget.Auxio.TextView.Header"
|
style="@style/Widget.Auxio.TextView.Header"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{header.name}"
|
android:text="@{header.string.resolve(context)}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{@string/fmt_two(album.artist.name, @plurals/fmt_song_count(album.songs.size, album.songs.size))}"
|
android:text="@{@string/fmt_two(album.artist.resolvedName, @plurals/fmt_song_count(album.songs.size, album.songs.size))}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/artist_image"
|
android:id="@+id/artist_image"
|
||||||
style="@style/Widget.Auxio.Image.Normal"
|
style="@style/Widget.Auxio.Image.Normal"
|
||||||
android:contentDescription="@{@string/desc_artist_image(artist.name)}"
|
android:contentDescription="@{@string/desc_artist_image(artist.resolvedName)}"
|
||||||
app:artistImage="@{artist}"
|
app:artistImage="@{artist}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Widget.Auxio.TextView.Item.Primary"
|
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||||
android:text="@{artist.name}"
|
android:text="@{artist.resolvedName}"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/artist_details"
|
app:layout_constraintBottom_toTopOf="@+id/artist_details"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/artist_image"
|
app:layout_constraintStart_toEndOf="@+id/artist_image"
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/album_name"
|
android:id="@+id/album_name"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Widget.Auxio.TextView.Item.Primary"
|
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||||
android:textColor="@color/sel_accented_primary"
|
android:textColor="@color/sel_accented_primary"
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/album_year"
|
android:id="@+id/album_year"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||||
android:text="@{album.year != 0 ? String.valueOf(album.year) : @string/def_date}"
|
android:text="@{album.year != 0 ? String.valueOf(album.year) : @string/def_date}"
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/genre_image"
|
android:id="@+id/genre_image"
|
||||||
style="@style/Widget.Auxio.Image.Normal"
|
style="@style/Widget.Auxio.Image.Normal"
|
||||||
android:contentDescription="@{@string/desc_genre_image(genre.name)}"
|
android:contentDescription="@{@string/desc_genre_image(genre.resolvedName)}"
|
||||||
app:genreImage="@{genre}"
|
app:genreImage="@{genre}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
|
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/song_duration"
|
app:layout_constraintEnd_toStartOf="@+id/song_duration"
|
||||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/song_duration"
|
android:id="@+id/song_duration"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||||
android:ellipsize="none"
|
android:ellipsize="none"
|
||||||
|
|
|
@ -15,6 +15,6 @@
|
||||||
style="@style/Widget.Auxio.TextView.Header"
|
style="@style/Widget.Auxio.TextView.Header"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:text="@{header.name}"
|
android:text="@{header.string.resolve(context)}"
|
||||||
tools:text="Songs" />
|
tools:text="Songs" />
|
||||||
</layout>
|
</layout>
|
|
@ -69,7 +69,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
|
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/song_drag_handle"
|
app:layout_constraintEnd_toStartOf="@+id/song_drag_handle"
|
||||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
|
android:text="@{@string/fmt_two(song.album.artist.resolvedName, song.album.name)}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
|
|
@ -41,4 +41,9 @@
|
||||||
|
|
||||||
<dimen name="popup_min_width">78dp</dimen>
|
<dimen name="popup_min_width">78dp</dimen>
|
||||||
<dimen name="popup_padding_end">28dp</dimen>
|
<dimen name="popup_padding_end">28dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="widget_width_min">176dp</dimen>
|
||||||
|
<dimen name="widget_height_min">180dp</dimen>
|
||||||
|
<dimen name="widget_width_def">@dimen/widget_width_min</dimen>
|
||||||
|
<dimen name="widget_height_def">180dp</dimen>
|
||||||
</resources>
|
</resources>
|
|
@ -134,8 +134,8 @@
|
||||||
<string name="desc_artist_image">Artist Image for %s</string>
|
<string name="desc_artist_image">Artist Image for %s</string>
|
||||||
<string name="desc_genre_image">Genre Image for %s</string>
|
<string name="desc_genre_image">Genre Image for %s</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Default Namespace | Placeholder values -->
|
<!-- Default Namespace | Placeholder values -->
|
||||||
|
<string name="def_artist">Unknown Artist</string>
|
||||||
<string name="def_genre">Unknown Genre</string>
|
<string name="def_genre">Unknown Genre</string>
|
||||||
<string name="def_date">No Date</string>
|
<string name="def_date">No Date</string>
|
||||||
<string name="def_playback">No music playing</string>
|
<string name="def_playback">No music playing</string>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:description="@string/info_widget_desc"
|
android:description="@string/info_widget_desc"
|
||||||
android:initialLayout="@layout/widget_small"
|
android:initialLayout="@layout/widget_minimal"
|
||||||
android:minResizeWidth="176dp"
|
android:minResizeWidth="@dimen/widget_width_min"
|
||||||
android:minResizeHeight="152dp"
|
android:minResizeHeight="@dimen/widget_height_min"
|
||||||
android:previewLayout="@layout/widget_small"
|
android:previewLayout="@layout/widget_minimal"
|
||||||
android:previewImage="@drawable/ui_widget_preview"
|
android:previewImage="@drawable/ui_widget_preview"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:minWidth="176dp"
|
android:minWidth="@dimen/widget_width_def"
|
||||||
android:minHeight="180dp"
|
android:minHeight="@dimen/widget_height_def"
|
||||||
android:targetCellWidth="3"
|
android:targetCellWidth="3"
|
||||||
android:targetCellHeight="3"
|
android:targetCellHeight="3"
|
||||||
android:updatePeriodMillis="0"
|
android:updatePeriodMillis="0"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:initialLayout="@layout/widget_small"
|
android:initialLayout="@layout/widget_minimal"
|
||||||
android:minWidth="176dp"
|
android:minWidth="@dimen/widget_width_min"
|
||||||
android:minHeight="180dp"
|
android:minHeight="@dimen/widget_height_min"
|
||||||
android:minResizeWidth="176dp"
|
android:minResizeWidth="@dimen/widget_width_def"
|
||||||
android:minResizeHeight="152dp"
|
android:minResizeHeight="@dimen/widget_height_def"
|
||||||
android:previewImage="@drawable/ui_widget_preview"
|
android:previewImage="@drawable/ui_widget_preview"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:updatePeriodMillis="0"
|
android:updatePeriodMillis="0"
|
||||||
|
|
Loading…
Reference in a new issue