From df49e2765f83351172bcf77301f12610e278dcd0 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Thu, 28 Oct 2021 19:09:54 -0600 Subject: [PATCH] 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 --- .../java/org/oxycblt/auxio/coil/CoilUtils.kt | 5 +- .../auxio/detail/AlbumDetailFragment.kt | 2 +- .../auxio/detail/ArtistDetailFragment.kt | 2 +- .../oxycblt/auxio/detail/DetailFragment.kt | 6 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 36 ++--- .../auxio/detail/GenreDetailFragment.kt | 2 +- .../detail/recycler/AlbumDetailAdapter.kt | 2 +- .../detail/recycler/ArtistDetailAdapter.kt | 7 +- .../detail/recycler/GenreDetailAdapter.kt | 7 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 8 +- .../auxio/home/list/AlbumListFragment.kt | 2 +- .../auxio/home/list/ArtistListFragment.kt | 3 +- .../auxio/home/list/GenreListFragment.kt | 3 +- .../auxio/home/list/SongListFragment.kt | 5 +- .../java/org/oxycblt/auxio/music/Models.kt | 139 ++++++++++++------ .../org/oxycblt/auxio/music/MusicLoader.kt | 15 +- .../auxio/playback/PlaybackFragment.kt | 6 +- .../auxio/playback/PlaybackViewModel.kt | 79 ++++++++-- .../auxio/playback/queue/QueueAdapter.kt | 2 - .../auxio/playback/queue/QueueFragment.kt | 55 +------ .../playback/system/PlaybackNotification.kt | 4 +- .../system/PlaybackSessionConnector.kt | 10 +- .../org/oxycblt/auxio/search/SearchAdapter.kt | 5 +- .../oxycblt/auxio/search/SearchFragment.kt | 9 +- .../oxycblt/auxio/search/SearchViewModel.kt | 32 +--- .../java/org/oxycblt/auxio/ui/SortMode.kt | 49 +++--- .../res/layout-land/fragment_playback.xml | 2 +- .../layout-xlarge-land/fragment_playback.xml | 2 +- .../res/layout-xlarge/fragment_playback.xml | 2 +- .../res/layout/fragment_compact_playback.xml | 2 +- app/src/main/res/layout/fragment_playback.xml | 2 +- .../main/res/layout/item_action_header.xml | 2 +- app/src/main/res/layout/item_album.xml | 2 +- app/src/main/res/layout/item_artist.xml | 4 +- app/src/main/res/layout/item_artist_album.xml | 4 +- app/src/main/res/layout/item_genre.xml | 2 +- app/src/main/res/layout/item_genre_song.xml | 4 +- app/src/main/res/layout/item_header.xml | 2 +- app/src/main/res/layout/item_queue_song.xml | 2 +- app/src/main/res/layout/item_song.xml | 2 +- app/src/main/res/layout/widget_minimal.xml | 2 +- app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml-v31/widget_info.xml | 12 +- app/src/main/res/xml/widget_info.xml | 10 +- 45 files changed, 315 insertions(+), 245 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index 4d70cf9b0..e54a86b45 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -71,7 +71,10 @@ fun ImageView.bindGenreImage(genre: Genre?) { load(genre, R.drawable.ic_genre, MosaicFetcher(context)) if (genre != null) { - contentDescription = context.getString(R.string.desc_genre_image, genre.name) + contentDescription = context.getString( + R.string.desc_genre_image, + genre.resolvedName + ) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 66c3cd983..93a323dac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -52,7 +52,7 @@ class AlbumDetailFragment : DetailFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - detailModel.setAlbum(args.albumId, requireContext()) + detailModel.setAlbum(args.albumId) val detailAdapter = AlbumDetailAdapter( playbackModel, detailModel, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 4311d5485..930866ef9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -48,7 +48,7 @@ class ArtistDetailFragment : DetailFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - detailModel.setArtist(args.artistId, requireContext()) + detailModel.setArtist(args.artistId) val detailAdapter = ArtistDetailAdapter( playbackModel, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 81cc1129b..584598f78 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -74,7 +74,7 @@ abstract class DetailFragment : Fragment() { super.onStop() // 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 -> item.isChecked = true - detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!, config.anchor.context) + detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!) true } setOnDismissListener { - detailModel.finishShowMenu(null, config.anchor.context) + detailModel.finishShowMenu(null) } if (showItem != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 5a98d59a2..069477718 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.detail -import android.content.Context import android.view.View import androidx.lifecycle.LiveData 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.Genre import org.oxycblt.auxio.music.Header +import org.oxycblt.auxio.music.HeaderString import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode @@ -80,50 +80,50 @@ class DetailViewModel : ViewModel() { private val settingsManager = SettingsManager.getInstance() - fun setGenre(id: Long, context: Context) { + fun setGenre(id: Long) { if (mCurGenre.value?.id == id) return val musicStore = MusicStore.requireInstance() 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 val musicStore = MusicStore.requireInstance() 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 val musicStore = MusicStore.requireInstance() mCurAlbum.value = musicStore.albums.find { it.id == id } - refreshAlbumData(context) + refreshAlbumData() } /** * Mark that the menu process is done with the new [SortMode]. * Pass null if there was no change. */ - fun finishShowMenu(newMode: SortMode?, context: Context) { + fun finishShowMenu(newMode: SortMode?) { mShowMenu.value = null if (newMode != null) { when (currentMenuContext) { DisplayMode.SHOW_ALBUMS -> { settingsManager.detailAlbumSort = newMode - refreshAlbumData(context) + refreshAlbumData() } DisplayMode.SHOW_ARTISTS -> { settingsManager.detailArtistSort = newMode - refreshArtistData(context) + refreshArtistData() } DisplayMode.SHOW_GENRES -> { settingsManager.detailGenreSort = newMode - refreshGenreData(context) + refreshGenreData() } else -> {} } @@ -153,13 +153,13 @@ class DetailViewModel : ViewModel() { isNavigating = navigating } - private fun refreshGenreData(context: Context) { + private fun refreshGenreData() { val data = mutableListOf(curGenre.value!!) data.add( ActionHeader( id = -2, - name = context.getString(R.string.lbl_songs), + string = HeaderString.Single(R.string.lbl_songs), icon = R.drawable.ic_sort, desc = R.string.lbl_sort, onClick = { view -> @@ -174,14 +174,14 @@ class DetailViewModel : ViewModel() { mGenreData.value = data } - private fun refreshArtistData(context: Context) { + private fun refreshArtistData() { val artist = curArtist.value!! val data = mutableListOf(artist) data.add( Header( 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( ActionHeader( id = -3, - name = context.getString(R.string.lbl_songs), + string = HeaderString.Single(R.string.lbl_songs), icon = R.drawable.ic_sort, desc = R.string.lbl_sort, onClick = { view -> @@ -205,13 +205,13 @@ class DetailViewModel : ViewModel() { mArtistData.value = data.toList() } - private fun refreshAlbumData(context: Context) { + private fun refreshAlbumData() { val data = mutableListOf(curAlbum.value!!) data.add( ActionHeader( id = -2, - name = context.getString(R.string.lbl_songs), + string = HeaderString.Single(R.string.lbl_songs), icon = R.drawable.ic_sort, desc = R.string.lbl_sort, onClick = { view -> diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 3aab76b95..15b92f92e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -48,7 +48,7 @@ class GenreDetailFragment : DetailFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - detailModel.setGenre(args.genreId, requireContext()) + detailModel.setGenre(args.genreId) val detailAdapter = GenreDetailAdapter( playbackModel, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index b4eb195bf..3ca43588a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -145,7 +145,7 @@ class AlbumDetailAdapter( binding.detailName.text = data.name binding.detailSubhead.apply { - text = data.artist.name + text = data.artist.resolvedName setOnClickListener { detailModel.navToItem(data.artist) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index 3ff57a6e2..dbdda7ae8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -190,10 +190,13 @@ class ArtistDetailAdapter( binding.detailCover.apply { 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 ?: context.getString(R.string.def_genre) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 7b642f93b..f77ebbdd1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -136,10 +136,13 @@ class GenreDetailAdapter( binding.detailCover.apply { 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 { text = context.getPlural(R.plurals.fmt_song_count, data.songs.size) diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 808c010ef..fe82693e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -112,11 +112,11 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal } DisplayMode.SHOW_ARTISTS -> { settingsManager.libArtistSort = sort - mArtists.value = sort.sortModels(mArtists.value!!) + mArtists.value = sort.sortParents(mArtists.value!!) } DisplayMode.SHOW_GENRES -> { 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) { mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) - mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists) - mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres) + mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists) + mGenres.value = settingsManager.libGenreSort.sortParents(musicStore.genres) } override fun onCleared() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 4a3c7a76c..a82611d41 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -62,7 +62,7 @@ class AlbumListFragment : HomeListFragment() { SortMode.ASCENDING, SortMode.DESCENDING -> album.name.sliceArticle() .first().uppercase() - SortMode.ARTIST -> album.artist.name.sliceArticle() + SortMode.ARTIST -> album.artist.resolvedName.sliceArticle() .first().uppercase() SortMode.YEAR -> album.year.toString() diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index dccc3fd86..7b8622a54 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -54,7 +54,8 @@ class ArtistListFragment : HomeListFragment() { override val popupProvider: (Int) -> String get() = { idx -> - homeModel.artists.value!![idx].name.sliceArticle().first().uppercase() + homeModel.artists.value!![idx].resolvedName + .sliceArticle().first().uppercase() } class ArtistAdapter( diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index d04d513cb..647f91502 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -54,7 +54,8 @@ class GenreListFragment : HomeListFragment() { override val popupProvider: (Int) -> String get() = { idx -> - homeModel.genres.value!![idx].name.sliceArticle().first().uppercase() + homeModel.genres.value!![idx].resolvedName + .sliceArticle().first().uppercase() } class GenreAdapter( diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 5b4e4cadf..89a01d9d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -58,8 +58,9 @@ class SongListFragment : HomeListFragment() { SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle() .first().uppercase() - SortMode.ARTIST -> song.album.artist.name.sliceArticle() - .first().uppercase() + SortMode.ARTIST -> + song.album.artist.resolvedName + .sliceArticle().first().uppercase() SortMode.ALBUM -> song.album.name.sliceArticle() .first().uppercase() diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index 9cf8f5531..d35afcadb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.music +import android.content.Context import android.view.View import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -27,46 +28,22 @@ import androidx.annotation.StringRes /** * The base data object for all music. * @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 { abstract val id: Long - abstract val name: String } -/** - * Provides a versatile static hash for a music item that will not change when - * MediaStore changes. - * - * 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 +sealed class Music : BaseModel() { + abstract val name: String + abstract val hash: Int } /** * [BaseModel] variant that denotes that this object is a parent of other data objects, such * 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 { - val displayName: String get() = if (this is Genre) { - resolvedName - } else { - name - } +sealed class Parent : Music() { + abstract val resolvedName: String } /** @@ -92,7 +69,7 @@ data class Song( val year: Int, val track: Int, val duration: Long -) : BaseModel(), Hashable { +) : Music() { private var mAlbum: Album? = null private var mGenre: Genre? = null @@ -145,6 +122,10 @@ data class Album( val totalDuration: String get() = songs.sumOf { it.seconds }.toDuration() + fun linkArtist(artist: Artist) { + mArtist = artist + } + override val hash: Int get() { var result = name.hashCode() result = 31 * result + artistName.hashCode() @@ -152,9 +133,8 @@ data class Album( return result } - fun linkArtist(artist: Artist) { - mArtist = artist - } + override val resolvedName: String + get() = name } /** @@ -166,6 +146,7 @@ data class Album( data class Artist( override val id: Long, override val name: String, + override val resolvedName: String, val albums: List ) : Parent() { init { @@ -190,44 +171,114 @@ data class Artist( /** * The data object for a genre. Inherits [Parent] * @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( override val id: Long, override val name: String, + override val resolvedName: String ) : Parent() { private val mSongs = mutableListOf() val songs: List get() = mSongs - val resolvedName = - name.getGenreNameCompat() ?: name - val totalDuration: String get() = songs.sumOf { it.seconds }.toDuration() - override val hash = name.hashCode() - fun linkSong(song: Song) { mSongs.add(song) 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. + * @see HeaderString */ data class Header( override val id: Long, - override val name: String, + val string: HeaderString ) : BaseModel() /** * A data object used for an action header. Like [Header], but with a button. - * Inherits [BaseModel]. + * @see HeaderString */ data class ActionHeader( override val id: Long, - override val name: String, + val string: HeaderString, @DrawableRes val icon: Int, @StringRes val desc: Int, val onClick: (View) -> Unit, @@ -239,7 +290,7 @@ data class ActionHeader( if (other !is ActionHeader) 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 (desc != other.desc) return false @@ -248,7 +299,7 @@ data class ActionHeader( override fun hashCode(): Int { var result = id.hashCode() - result = 31 * result + name.hashCode() + result = 31 * result + string.hashCode() result = 31 * result + icon result = 31 * result + desc diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index 561bc4c07..24e40cca1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music import android.annotation.SuppressLint import android.content.Context +import android.provider.MediaStore import android.provider.MediaStore.Audio.Genres import android.provider.MediaStore.Audio.Media import androidx.core.database.getStringOrNull @@ -152,7 +153,7 @@ class MusicLoader(private val context: Context) { // No non-broken genre would be missing a name. 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 } 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. // Therefore we just use the hashCode of the artist name as our ID and move on. artists.add( Artist( id = entry.key.hashCode().toLong(), name = entry.key, + resolvedName = resolvedName, albums = entry.value ) ) } - artists = SortMode.ASCENDING.sortModels(artists).toMutableList() + artists = SortMode.ASCENDING.sortParents(artists).toMutableList() logD("Albums successfully linked into ${artists.size} artists") } @@ -304,7 +312,8 @@ class MusicLoader(private val context: Context) { val unknownGenre = Genre( 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 -> diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 6ddc0c208..3c2f6acb1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -122,11 +122,7 @@ class PlaybackFragment : Fragment() { binding.playbackSeekBar.setProgress(pos) } - playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { - updateQueueIcon(queueItem) - } - - playbackModel.userQueue.observe(viewLifecycleOwner) { + playbackModel.displayQueue.observe(viewLifecycleOwner) { updateQueueIcon(queueItem) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 3e312975a..2e3112c83 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -21,14 +21,20 @@ package org.oxycblt.auxio.playback import android.content.Context import android.net.Uri import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.Artist +import org.oxycblt.auxio.music.BaseModel 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.Parent import org.oxycblt.auxio.music.Song @@ -89,16 +95,72 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { /** The current repeat mode, see [LoopMode] for more information */ val loopMode: LiveData 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>().apply { + val combine: (userQueue: List, nextQueue: List) -> List = + { userQueue, nextQueue -> + val queue = mutableListOf() + + 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. */ val positionAsProgress = Transformations.map(mPosition) { 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 settingsManager = SettingsManager.getInstance() @@ -316,13 +378,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.addToUserQueue(settingsManager.detailAlbumSort.sortAlbum(album)) } - /** - * Clear the user queue entirely - */ - fun clearUserQueue() { - playbackManager.clearUserQueue() - } - // --- STATUS FUNCTIONS --- /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 22d949684..8f74f463c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -115,8 +115,6 @@ class QueueAdapter( fun removeItem(adapterIndex: Int) { data.removeAt(adapterIndex) - logD(data) - /* * 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. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 5fa1d0c15..4c1b46531 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -27,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ItemTouchHelper -import org.oxycblt.auxio.R 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.util.applyEdge @@ -79,22 +75,13 @@ class QueueFragment : Fragment() { // --- VIEWMODEL SETUP ---- - playbackModel.userQueue.observe(viewLifecycleOwner) { userQueue -> - if (userQueue.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) { + playbackModel.displayQueue.observe(viewLifecycleOwner) { queue -> + if (queue.isEmpty()) { findNavController().navigateUp() - return@observe } - queueAdapter.submitList(createQueueData()) - } - - playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { nextQueue -> - if (nextQueue.isEmpty() && playbackModel.userQueue.value!!.isEmpty()) { - findNavController().navigateUp() - } - - queueAdapter.submitList(createQueueData()) + queueAdapter.submitList(queue.toMutableList()) } playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling -> @@ -109,40 +96,4 @@ class QueueFragment : Fragment() { } // --- QUEUE DATA --- - - /** - * Create the queue data that should be displayed - * @return The list of headers/songs that should be displayed. - */ - private fun createQueueData(): MutableList { - val queue = mutableListOf() - 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 - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt index 59091f0e2..ff7f23123 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt @@ -81,7 +81,7 @@ class PlaybackNotification private constructor( */ fun setMetadata(song: Song, onDone: () -> Unit) { 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 // 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 // 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 --- diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt index c2d557b9a..e2cd189c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt @@ -114,13 +114,15 @@ class PlaybackSessionConnector( return } + val artistName = song.album.artist.resolvedName + val builder = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name) - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.album.artist.name) - .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, song.album.artist.name) - .putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, song.album.artist.name) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.album.artist.name) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 64192ce59..48b780591 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Header +import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.AlbumViewHolder import org.oxycblt.auxio.ui.ArtistViewHolder @@ -40,8 +41,8 @@ import org.oxycblt.auxio.ui.SongViewHolder * @author OxygenCobalt */ class SearchAdapter( - private val doOnClick: (data: BaseModel) -> Unit, - private val doOnLongClick: (view: View, data: BaseModel) -> Unit + private val doOnClick: (data: Music) -> Unit, + private val doOnLongClick: (view: View, data: Music) -> Unit ) : ListAdapter(DiffCallback()) { override fun getItemViewType(position: Int): Int { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index cd3dabbd4..c9ab03b15 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -35,9 +35,9 @@ import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Header +import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.DisplayMode @@ -54,10 +54,7 @@ import org.oxycblt.auxio.util.logD */ class SearchFragment : Fragment() { // SearchViewModel is only scoped to this Fragment - private val searchModel: SearchViewModel by viewModels { - SearchViewModel.Factory(requireContext()) - } - + private val searchModel: SearchViewModel by viewModels() private val playbackModel: PlaybackViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -183,7 +180,7 @@ class SearchFragment : Fragment() { * Function that handles when an [item] is selected. * Handles all datatypes that are selectable. */ - private fun onItemSelection(item: BaseModel, imm: InputMethodManager) { + private fun onItemSelection(item: Music, imm: InputMethodManager) { if (item is Song) { playbackModel.playSong(item) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 117336d21..f206b5e3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -18,17 +18,17 @@ package org.oxycblt.auxio.search -import android.content.Context import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.music.BaseModel 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.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode @@ -38,7 +38,7 @@ import java.text.Normalizer * The [ViewModel] for the search functionality * @author OxygenCobalt */ -class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback { +class SearchViewModel : ViewModel(), MusicStore.MusicCallback { private val mSearchResults = MutableLiveData(listOf()) private var mIsNavigating = false private var mFilterMode: DisplayMode? = null @@ -51,11 +51,6 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback 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 { mFilterMode = settingsManager.searchFilterMode @@ -83,28 +78,28 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) { musicStore.artists.filterByOrNull(query)?.let { artists -> - results.add(artistHeader) + results.add(Header(-1, HeaderString.Single(R.string.lbl_artists))) results.addAll(artists) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) { musicStore.albums.filterByOrNull(query)?.let { albums -> - results.add(albumHeader) + results.add(Header(-1, HeaderString.Single(R.string.lbl_albums))) results.addAll(albums) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) { musicStore.genres.filterByOrNull(query)?.let { genres -> - results.add(genreHeader) + results.add(Header(-1, HeaderString.Single(R.string.lbl_genres))) results.addAll(genres) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) { musicStore.songs.filterByOrNull(query)?.let { songs -> - results.add(songHeader) + results.add(Header(-1, HeaderString.Single(R.string.lbl_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 * a value if the resulting list is empty. */ - private fun List.filterByOrNull(value: String): List? { + private fun List.filterByOrNull(value: String): List? { val filtered = filter { // 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 @@ -195,15 +190,4 @@ class SearchViewModel(context: Context) : ViewModel(), MusicStore.MusicCallback super.onCleared() MusicStore.cancelAwaitInstance(this) } - - class Factory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - check(modelClass.isAssignableFrom(SearchViewModel::class.java)) { - "SearchViewModel.Factory does not support this class" - } - - @Suppress("UNCHECKED_CAST") - return SearchViewModel(context) as T - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt b/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt index f50ab4fad..3b3505664 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt @@ -22,8 +22,8 @@ import androidx.annotation.IdRes import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.Song /** @@ -44,7 +44,8 @@ enum class SortMode(@IdRes val itemId: Int) { * Sort a list of songs. * * **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. * - [ALBUM]: Grouped by album and sorted [ASCENDING] * - [YEAR]: Grouped by album and sorted by year @@ -54,7 +55,17 @@ enum class SortMode(@IdRes val itemId: Int) { */ fun sortSongs(songs: Collection): List { 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 -> ASCENDING.sortAlbum(album) @@ -66,7 +77,8 @@ enum class SortMode(@IdRes val itemId: Int) { * Sort a list of albums. * * **Behavior:** - * - [ASCENDING] & [DESCENDING]: See [sortModels] + * - [ASCENDING]: By name after article, ascending + * - [DESCENDING]: By name after article, descending * - [ARTIST]: Grouped by artist and sorted [ASCENDING] * - [ALBUM]: [ASCENDING] * - [YEAR]: Sorted by year @@ -75,43 +87,40 @@ enum class SortMode(@IdRes val itemId: Int) { */ fun sortAlbums(albums: Collection): List { 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) } - ALBUM -> ASCENDING.sortModels(albums) + ALBUM -> ASCENDING.sortParents(albums) YEAR -> albums.sortedByDescending { it.year } } } /** - * Sort a list of generic [BaseModel] instances. + * Sort a generic list of [Parent] instances. * * **Behavior:** - * - [ASCENDING]: Sorted by name, ascending - * - [DESCENDING]: Sorted by name, descending - * - Same 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. + * - [ASCENDING]: By name after article, ascending + * - [DESCENDING]: By name after article, descending + * - Same parent list is returned otherwise. */ - fun sortModels(models: Collection): List { + fun sortParents(parents: Collection): List { return when (this) { - ASCENDING -> models.sortedWith( + ASCENDING -> parents.sortedWith( compareBy(String.CASE_INSENSITIVE_ORDER) { model -> - model.name.sliceArticle() + model.resolvedName.sliceArticle() } ) - DESCENDING -> models.sortedWith( + DESCENDING -> parents.sortedWith( compareByDescending(String.CASE_INSENSITIVE_ORDER) { model -> - model.name.sliceArticle() + model.resolvedName.sliceArticle() } ) - else -> models.toList() + else -> parents.toList() } } diff --git a/app/src/main/res/layout-land/fragment_playback.xml b/app/src/main/res/layout-land/fragment_playback.xml index 417613444..c998bb795 100644 --- a/app/src/main/res/layout-land/fragment_playback.xml +++ b/app/src/main/res/layout-land/fragment_playback.xml @@ -80,7 +80,7 @@ android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_mid_large" 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_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" diff --git a/app/src/main/res/layout-xlarge-land/fragment_playback.xml b/app/src/main/res/layout-xlarge-land/fragment_playback.xml index dd88ab917..a81129c05 100644 --- a/app/src/main/res/layout-xlarge-land/fragment_playback.xml +++ b/app/src/main/res/layout-xlarge-land/fragment_playback.xml @@ -82,7 +82,7 @@ android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_mid_large" 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_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" diff --git a/app/src/main/res/layout-xlarge/fragment_playback.xml b/app/src/main/res/layout-xlarge/fragment_playback.xml index 5a4ed2a8e..8333f11e7 100644 --- a/app/src/main/res/layout-xlarge/fragment_playback.xml +++ b/app/src/main/res/layout-xlarge/fragment_playback.xml @@ -70,7 +70,7 @@ android:layout_marginStart="@dimen/spacing_mid_huge" android:layout_marginEnd="@dimen/spacing_mid_huge" 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_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_compact_playback.xml b/app/src/main/res/layout/fragment_compact_playback.xml index 9f60d5658..3b88f2b9d 100644 --- a/app/src/main/res/layout/fragment_compact_playback.xml +++ b/app/src/main/res/layout/fragment_compact_playback.xml @@ -58,7 +58,7 @@ android:layout_marginStart="@dimen/spacing_small" android:layout_marginEnd="@dimen/spacing_small" 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_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintStart_toEndOf="@+id/playback_cover" diff --git a/app/src/main/res/layout/fragment_playback.xml b/app/src/main/res/layout/fragment_playback.xml index 280cefba2..339513ec1 100644 --- a/app/src/main/res/layout/fragment_playback.xml +++ b/app/src/main/res/layout/fragment_playback.xml @@ -69,7 +69,7 @@ android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_mid_large" 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_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/item_action_header.xml b/app/src/main/res/layout/item_action_header.xml index d1f39eeb5..fdc1429dc 100644 --- a/app/src/main/res/layout/item_action_header.xml +++ b/app/src/main/res/layout/item_action_header.xml @@ -20,7 +20,7 @@ style="@style/Widget.Auxio.TextView.Header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@{header.name}" + android:text="@{header.string.resolve(context)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/item_album.xml b/app/src/main/res/layout/item_album.xml index d333c446e..be1d8103a 100644 --- a/app/src/main/res/layout/item_album.xml +++ b/app/src/main/res/layout/item_album.xml @@ -41,7 +41,7 @@ style="@style/Widget.Auxio.TextView.Item.Secondary" android:layout_width="0dp" 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_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/album_cover" diff --git a/app/src/main/res/layout/item_artist.xml b/app/src/main/res/layout/item_artist.xml index 3adba9334..90d1fd34a 100644 --- a/app/src/main/res/layout/item_artist.xml +++ b/app/src/main/res/layout/item_artist.xml @@ -16,7 +16,7 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/item_queue_song.xml b/app/src/main/res/layout/item_queue_song.xml index b84eca75e..935fdb630 100644 --- a/app/src/main/res/layout/item_queue_song.xml +++ b/app/src/main/res/layout/item_queue_song.xml @@ -69,7 +69,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" 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_constraintEnd_toStartOf="@+id/song_drag_handle" app:layout_constraintStart_toEndOf="@+id/album_cover" diff --git a/app/src/main/res/layout/item_song.xml b/app/src/main/res/layout/item_song.xml index 78a2f875c..bd4e06b5b 100644 --- a/app/src/main/res/layout/item_song.xml +++ b/app/src/main/res/layout/item_song.xml @@ -42,7 +42,7 @@ style="@style/Widget.Auxio.TextView.Item.Secondary" android:layout_width="0dp" 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_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/album_cover" diff --git a/app/src/main/res/layout/widget_minimal.xml b/app/src/main/res/layout/widget_minimal.xml index 2fae4ae0c..5d3cb9495 100644 --- a/app/src/main/res/layout/widget_minimal.xml +++ b/app/src/main/res/layout/widget_minimal.xml @@ -82,4 +82,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 961d50943..e0e468bd8 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -41,4 +41,9 @@ 78dp 28dp + + 176dp + 180dp + @dimen/widget_width_min + 180dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fdb3ceaf3..266be010c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,8 +134,8 @@ Artist Image for %s Genre Image for %s - + Unknown Artist Unknown Genre No Date No music playing diff --git a/app/src/main/res/xml-v31/widget_info.xml b/app/src/main/res/xml-v31/widget_info.xml index 08447a994..4890660a1 100644 --- a/app/src/main/res/xml-v31/widget_info.xml +++ b/app/src/main/res/xml-v31/widget_info.xml @@ -1,14 +1,14 @@