music: rework id system
Completely rework the ID system to pave the way to MusicBrainz ID support and greatly increase ID integrity in general. This changeset removes the old ID field, an emulation of a polynomial hash that was used in all items, and replaces it with a new type called UID that is specific to Music. Other types just use plain equals now, and most instances of "id" to check for equality in the app have either been inlined into an equals override or removed outright. The new UID format is as follows: datatype/format:uuid Datatype is a tag that is just the lowercase tag name. For example, "song". Format is the program that created the UID. auxio will be an md5 hash, and musicbrainz will the a musicbrainz ID extracted from a file. UUID is the uuid itself. This is much more reliable and extendable than the old ID format. This will also be the last time I break compat with old ID formats. From now on, a legacy UID field will not be included to enable backwards compat, when the time comes for a breaking change.
This commit is contained in:
parent
457013d047
commit
48ad45e4c3
31 changed files with 443 additions and 360 deletions
|
@ -2,6 +2,9 @@
|
|||
|
||||
## dev
|
||||
|
||||
#### What's New
|
||||
- Reworked music hashing to be even more reliable (Will wipe playback state)
|
||||
|
||||
#### What's Fixed
|
||||
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
||||
- Fixed issue where the playback progress would continue in the notification even if
|
||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
id "kotlin-android"
|
||||
id "androidx.navigation.safeargs.kotlin"
|
||||
id "com.diffplug.spotless"
|
||||
id "kotlin-parcelize"
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
@ -225,7 +225,7 @@ class MainFragment :
|
|||
findNavController().navigate(MainFragmentDirections.actionShowAbout())
|
||||
is MainNavigationAction.SongDetails ->
|
||||
findNavController()
|
||||
.navigate(MainFragmentDirections.actionShowDetails(action.song.id))
|
||||
.navigate(MainFragmentDirections.actionShowDetails(action.song.uid))
|
||||
}
|
||||
|
||||
navModel.finishMainNavigation()
|
||||
|
|
|
@ -74,7 +74,7 @@ class AlbumDetailFragment :
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setAlbumId(args.albumId)
|
||||
detailModel.setAlbumUid(args.albumUid)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_album_detail)
|
||||
|
@ -169,7 +169,7 @@ class AlbumDetailFragment :
|
|||
findNavController()
|
||||
.navigate(
|
||||
AlbumDetailFragmentDirections.actionShowArtist(
|
||||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
|
||||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid))
|
||||
}
|
||||
|
||||
private fun handleItemChange(album: Album?) {
|
||||
|
@ -187,28 +187,28 @@ class AlbumDetailFragment :
|
|||
// Songs should be scrolled to if the album matches, or a new detail
|
||||
// fragment should be launched otherwise.
|
||||
is Song -> {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item.id)
|
||||
scrollToItem(item)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||
}
|
||||
}
|
||||
|
||||
// If the album matches, no need to do anything. Otherwise launch a new
|
||||
// detail fragment.
|
||||
is Album -> {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) {
|
||||
logD("Navigating to the top of this album")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,17 +216,17 @@ class AlbumDetailFragment :
|
|||
is Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
|
||||
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.uid))
|
||||
}
|
||||
null -> {}
|
||||
else -> error("Unexpected navigation item ${item::class.java}")
|
||||
}
|
||||
}
|
||||
|
||||
/** Scroll to an song using its [id]. */
|
||||
private fun scrollToItem(id: Long) {
|
||||
/** Scroll to an [song]. */
|
||||
private fun scrollToItem(song: Song) {
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = detailModel.albumData.value.indexOfFirst { it.id == id && it is Song }
|
||||
val pos = detailModel.albumData.value.indexOf(song)
|
||||
|
||||
if (pos != -1) {
|
||||
val binding = requireBinding()
|
||||
|
@ -255,7 +255,7 @@ class AlbumDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
|
@ -279,8 +279,6 @@ class AlbumDetailFragment :
|
|||
boxStart: Int,
|
||||
boxEnd: Int,
|
||||
snapPreference: Int
|
||||
): Int {
|
||||
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
||||
}
|
||||
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ class ArtistDetailFragment :
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setArtistId(args.artistId)
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
|
@ -177,22 +177,22 @@ class ArtistDetailFragment :
|
|||
is Song -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||
}
|
||||
is Artist -> {
|
||||
if (item.id == detailModel.currentArtist.value?.id) {
|
||||
if (item.uid == detailModel.currentArtist.value?.uid) {
|
||||
logD("Navigating to the top of this artist")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.uid))
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
|
@ -207,7 +207,7 @@ class ArtistDetailFragment :
|
|||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
|
||||
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
|
||||
item = song
|
||||
}
|
||||
|
||||
|
|
|
@ -109,10 +109,10 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
private val songGuard = TaskGuard()
|
||||
|
||||
fun setSongId(id: Long) {
|
||||
if (_currentSong.value?.run { song.id } == id) return
|
||||
fun setSongUid(uid: Music.UID) {
|
||||
if (_currentSong.value?.run { song.uid } == uid) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val song = requireNotNull(library.findSongById(id)) { "Invalid song id provided" }
|
||||
val song = requireNotNull(library.find<Song>(uid)) { "Invalid song id provided" }
|
||||
generateDetailSong(song)
|
||||
}
|
||||
|
||||
|
@ -121,27 +121,28 @@ class DetailViewModel(application: Application) :
|
|||
_currentSong.value = null
|
||||
}
|
||||
|
||||
fun setAlbumId(id: Long) {
|
||||
if (_currentAlbum.value?.id == id) return
|
||||
fun setAlbumUid(uid: Music.UID) {
|
||||
if (_currentAlbum.value?.uid == uid) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val album = requireNotNull(library.findAlbumById(id)) { "Invalid album id provided " }
|
||||
val album = requireNotNull(library.find<Album>(uid)) { "Invalid album id provided " }
|
||||
|
||||
_currentAlbum.value = album
|
||||
refreshAlbumData(album)
|
||||
}
|
||||
|
||||
fun setArtistId(id: Long) {
|
||||
if (_currentArtist.value?.id == id) return
|
||||
fun setArtistUid(uid: Music.UID) {
|
||||
logD(uid)
|
||||
if (_currentArtist.value?.uid == uid) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val artist = requireNotNull(library.findArtistById(id)) { "Invalid artist id provided" }
|
||||
val artist = requireNotNull(library.find<Artist>(uid)) { "Invalid artist id provided" }
|
||||
_currentArtist.value = artist
|
||||
refreshArtistData(artist)
|
||||
}
|
||||
|
||||
fun setGenreId(id: Long) {
|
||||
if (_currentGenre.value?.id == id) return
|
||||
fun setGenreUid(uid: Music.UID) {
|
||||
if (_currentGenre.value?.uid == uid) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val genre = requireNotNull(library.findGenreById(id)) { "Invalid genre id provided" }
|
||||
val genre = requireNotNull(library.find<Genre>(uid)) { "Invalid genre id provided" }
|
||||
_currentGenre.value = genre
|
||||
refreshGenreData(genre)
|
||||
}
|
||||
|
@ -318,12 +319,6 @@ class DetailViewModel(application: Application) :
|
|||
}
|
||||
}
|
||||
|
||||
data class SortHeader(@StringRes val string: Int) : Item() {
|
||||
override val id: Long
|
||||
get() = string.toLong()
|
||||
}
|
||||
data class SortHeader(@StringRes val string: Int) : Item
|
||||
|
||||
data class DiscHeader(val disc: Int) : Item() {
|
||||
override val id: Long
|
||||
get() = disc.toLong()
|
||||
}
|
||||
data class DiscHeader(val disc: Int) : Item
|
||||
|
|
|
@ -70,7 +70,7 @@ class GenreDetailFragment :
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||
detailModel.setGenreId(args.genreId)
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
|
||||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
|
@ -125,6 +125,7 @@ class GenreDetailFragment :
|
|||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
if (item is Song) {
|
||||
musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
return
|
||||
}
|
||||
|
||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
|
@ -170,16 +171,17 @@ class GenreDetailFragment :
|
|||
is Song -> {
|
||||
logD("Navigating to another song")
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||
}
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||
}
|
||||
is Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController()
|
||||
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
||||
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid))
|
||||
}
|
||||
is Genre -> {
|
||||
navModel.finishExploreNavigation()
|
||||
|
@ -189,7 +191,7 @@ class GenreDetailFragment :
|
|||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
|
||||
if (parent is Genre && parent == unlikelyToBeNull(detailModel.currentGenre.value)) {
|
||||
detailAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Ignore song playback not from the genre
|
||||
|
|
|
@ -50,7 +50,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
|
||||
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
detailModel.setSongId(args.songId)
|
||||
detailModel.setSongUid(args.songUid)
|
||||
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||
}
|
||||
|
||||
|
|
|
@ -85,15 +85,15 @@ class AlbumDetailAdapter(private val listener: Listener) :
|
|||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
AlbumDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is DiscHeader && newItem is DiscHeader ->
|
||||
DiscHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
DiscHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
AlbumSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
AlbumSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||
oldItem.date == newItem.date &&
|
||||
|
@ -168,7 +168,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<DiscHeader>() {
|
||||
override fun areItemsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
|
||||
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
|
||||
oldItem.disc == newItem.disc
|
||||
}
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,15 +87,15 @@ class ArtistDetailAdapter(private val listener: Listener) :
|
|||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
ArtistDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Album && newItem is Album ->
|
||||
ArtistAlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
ArtistAlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
ArtistSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
ArtistSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
|
||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||
// the most "Prominent" genre.
|
||||
var genresByAmount = mutableMapOf<Genre, Int>()
|
||||
val genresByAmount = mutableMapOf<Genre, Int>()
|
||||
for (song in item.songs) {
|
||||
for (genre in song.genres) {
|
||||
genresByAmount[genre] = genresByAmount[genre]?.inc() ?: 1
|
||||
|
@ -172,7 +172,7 @@ private constructor(
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName && oldItem.date == newItem.date
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ private constructor(
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.album.rawName == newItem.album.rawName
|
||||
}
|
||||
|
|
|
@ -95,12 +95,12 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
|||
companion object {
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Header && newItem is Header ->
|
||||
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is SortHeader && newItem is SortHeader ->
|
||||
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
SortHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<SortHeader>() {
|
||||
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
||||
override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,12 +79,12 @@ class GenreDetailAdapter(private val listener: Listener) :
|
|||
companion object {
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
GenreDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
GenreDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationMs == newItem.durationMs
|
||||
|
|
|
@ -372,10 +372,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
private fun handleNavigation(item: Music?) {
|
||||
val action =
|
||||
when (item) {
|
||||
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.id)
|
||||
is Album -> HomeFragmentDirections.actionShowAlbum(item.id)
|
||||
is Artist -> HomeFragmentDirections.actionShowArtist(item.id)
|
||||
is Genre -> HomeFragmentDirections.actionShowGenre(item.id)
|
||||
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid)
|
||||
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid.also { logD(it) })
|
||||
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
if (item is Artist) {
|
||||
musicMenu(anchor, R.menu.menu_genre_artist_actions, item)
|
||||
return
|
||||
}
|
||||
|
||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
|
|
|
@ -39,13 +39,12 @@ import org.oxycblt.auxio.ui.Sort
|
|||
|
||||
/** A basic keyer for music data. */
|
||||
class MusicKeyer : Keyer<Music> {
|
||||
override fun key(data: Music, options: Options): String {
|
||||
return if (data is Song) {
|
||||
override fun key(data: Music, options: Options) =
|
||||
if (data is Song) {
|
||||
// Group up song covers with album covers for better caching
|
||||
key(data.album, options)
|
||||
data.album.uid.toString()
|
||||
} else {
|
||||
"${data::class.simpleName}: ${data.id}"
|
||||
}
|
||||
data.uid.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,9 +64,8 @@ private constructor(private val context: Context, private val album: Album) : Ba
|
|||
}
|
||||
|
||||
class SongFactory : Fetcher.Factory<Song> {
|
||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return AlbumCoverFetcher(options.context, data.album)
|
||||
}
|
||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
|
||||
AlbumCoverFetcher(options.context, data.album)
|
||||
}
|
||||
|
||||
class AlbumFactory : Fetcher.Factory<Album> {
|
||||
|
@ -114,7 +112,7 @@ private constructor(
|
|||
// whenever possible. So, if there are more than four distinct artists in a genre, make
|
||||
// it so that one artist only adds one album cover to the mosaic. Otherwise, use order
|
||||
// albums normally.
|
||||
val artists = genre.songs.groupBy { it.album.artist.id }.keys
|
||||
val artists = genre.songs.groupBy { it.album.artist }.keys
|
||||
val albums =
|
||||
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
|
||||
if (artists.size > 4) {
|
||||
|
|
|
@ -20,20 +20,30 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.inRangeOrNull
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.msToSecs
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
// --- MUSIC MODELS ---
|
||||
|
||||
/** [Item] variant that represents a music item. */
|
||||
sealed class Music : Item() {
|
||||
sealed class Music : Item {
|
||||
abstract val uid: UID
|
||||
|
||||
/** The raw name of this item. Null if unknown. */
|
||||
abstract val rawName: String?
|
||||
|
||||
|
@ -52,6 +62,106 @@ sealed class Music : Item() {
|
|||
* become Unknown Artist, (124) would become its proper genre name, etc.
|
||||
*/
|
||||
abstract fun resolveName(context: Context): String
|
||||
|
||||
// Equality is based on UIDs, as some items (Especially artists) can have identical
|
||||
// properties (Name) yet non-identical UIDs due to MusicBrainz tags
|
||||
|
||||
override fun hashCode() = uid.hashCode()
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Music && javaClass == other.javaClass && uid == other.uid
|
||||
|
||||
/** A unique identifier for a piece of music. */
|
||||
@Parcelize
|
||||
class UID
|
||||
private constructor(val datatype: String, val isMusicBrainz: Boolean, val uuid: UUID) :
|
||||
Parcelable {
|
||||
@IgnoredOnParcel private val hashCode: Int
|
||||
|
||||
init {
|
||||
var result = datatype.hashCode()
|
||||
result = 31 * result + isMusicBrainz.hashCode()
|
||||
result = 31 * result + uuid.hashCode()
|
||||
hashCode = result
|
||||
}
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is UID &&
|
||||
datatype == other.datatype &&
|
||||
isMusicBrainz == other.isMusicBrainz &&
|
||||
uuid == other.uuid
|
||||
|
||||
override fun toString() = "$datatype/${if (isMusicBrainz) "musicbrainz" else "auxio"}:$uuid"
|
||||
|
||||
companion object {
|
||||
fun fromString(uid: String): UID? {
|
||||
val split = uid.split(':', limit = 2)
|
||||
if (split.size != 2) {
|
||||
logE("Invalid uid: Malformed structure")
|
||||
}
|
||||
|
||||
val namespace = split[0].split('/', limit = 2)
|
||||
if (namespace.size != 2) {
|
||||
logE("Invalid uid: Malformed namespace")
|
||||
return null
|
||||
}
|
||||
|
||||
val datatype = namespace[0]
|
||||
val isMusicBrainz =
|
||||
when (namespace[1]) {
|
||||
"auxio" -> false
|
||||
"musicbrainz" -> true
|
||||
else -> {
|
||||
logE("Invalid mid: Malformed uuid type")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val uuid =
|
||||
try {
|
||||
UUID.fromString(split[1])
|
||||
} catch (e: Exception) {
|
||||
logE("Invalid uid: Malformed UUID")
|
||||
return null
|
||||
}
|
||||
|
||||
return UID(datatype, isMusicBrainz, uuid)
|
||||
}
|
||||
|
||||
fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID {
|
||||
val digest = MessageDigest.getInstance("MD5")
|
||||
updates(digest)
|
||||
|
||||
val hash = digest.digest()
|
||||
val uuid =
|
||||
UUID(
|
||||
hash[0]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(hash[1].toLong().and(0xFF).shl(48))
|
||||
.or(hash[2].toLong().and(0xFF).shl(40))
|
||||
.or(hash[3].toLong().and(0xFF).shl(32))
|
||||
.or(hash[4].toLong().and(0xFF).shl(24))
|
||||
.or(hash[5].toLong().and(0xFF).shl(16))
|
||||
.or(hash[6].toLong().and(0xFF).shl(8))
|
||||
.or(hash[7].toLong().and(0xFF)),
|
||||
hash[8]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(hash[9].toLong().and(0xFF).shl(48))
|
||||
.or(hash[10].toLong().and(0xFF).shl(40))
|
||||
.or(hash[11].toLong().and(0xFF).shl(32))
|
||||
.or(hash[12].toLong().and(0xFF).shl(24))
|
||||
.or(hash[13].toLong().and(0xFF).shl(16))
|
||||
.or(hash[14].toLong().and(0xFF).shl(8))
|
||||
.or(hash[15].toLong().and(0xFF)))
|
||||
|
||||
return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,17 +177,8 @@ sealed class MusicParent : Music() {
|
|||
* A song.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Song(private val raw: Raw) : Music() {
|
||||
override val id: Long
|
||||
get() {
|
||||
var result = rawName.toMusicId()
|
||||
result = 31 * result + album.rawName.toMusicId()
|
||||
result = 31 * result + album.artist.rawName.toMusicId()
|
||||
result = 31 * result + (track ?: 0)
|
||||
result = 31 * result + (disc ?: 0)
|
||||
result = 31 * result + durationMs
|
||||
return result
|
||||
}
|
||||
class Song(private val raw: Raw) : Music() {
|
||||
override val uid: UID
|
||||
|
||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
||||
|
||||
|
@ -113,20 +214,18 @@ data class Song(private val raw: Raw) : Music() {
|
|||
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
||||
|
||||
/** The track number of this song in it's album.. */
|
||||
val track = raw.track
|
||||
val track: Int? = raw.track
|
||||
|
||||
/** The disc number of this song in it's album. */
|
||||
val disc = raw.disc
|
||||
val disc: Int? = raw.disc
|
||||
|
||||
private var _album: Album? = null
|
||||
/** The album of this song. */
|
||||
val album: Album
|
||||
get() = unlikelyToBeNull(_album)
|
||||
|
||||
private var _genres: MutableList<Genre> = mutableListOf()
|
||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||
val genres: List<Genre>
|
||||
get() = _genres
|
||||
// TODO: Multi-artist support
|
||||
// private val _artists: MutableList<Artist> = mutableListOf()
|
||||
|
||||
/**
|
||||
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
||||
|
@ -142,31 +241,29 @@ data class Song(private val raw: Raw) : Music() {
|
|||
fun resolveIndividualArtistName(context: Context) =
|
||||
raw.artistName ?: album.artist.resolveName(context)
|
||||
|
||||
private val _genres: MutableList<Genre> = mutableListOf()
|
||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||
val genres: List<Genre>
|
||||
get() = _genres
|
||||
|
||||
// --- INTERNAL FIELDS ---
|
||||
|
||||
val _distinct =
|
||||
rawName to
|
||||
raw.albumName to
|
||||
raw.artistName to
|
||||
raw.albumArtistName to
|
||||
raw.genreNames to
|
||||
track to
|
||||
disc to
|
||||
durationMs
|
||||
|
||||
val _rawAlbum: Album.Raw
|
||||
val _rawAlbum =
|
||||
Album.Raw(
|
||||
mediaStoreId = requireNotNull(raw.albumMediaStoreId) { "Invalid raw: No album id" },
|
||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||
sortName = raw.albumSortName,
|
||||
date = raw.date,
|
||||
releaseType = raw.albumReleaseType,
|
||||
rawArtist =
|
||||
if (raw.albumArtistName != null) {
|
||||
Artist.Raw(raw.albumArtistName, raw.albumArtistSortName)
|
||||
} else {
|
||||
Artist.Raw(raw.artistName, raw.artistSortName)
|
||||
})
|
||||
|
||||
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
||||
|
||||
val _isMissingAlbum: Boolean
|
||||
get() = _album == null
|
||||
|
||||
val _isMissingArtist: Boolean
|
||||
get() = _album?._isMissingArtist ?: true
|
||||
|
||||
val _isMissingGenre: Boolean
|
||||
get() = _genres.isEmpty()
|
||||
|
||||
fun _link(album: Album) {
|
||||
_album = album
|
||||
}
|
||||
|
@ -175,27 +272,28 @@ data class Song(private val raw: Raw) : Music() {
|
|||
_genres.add(genre)
|
||||
}
|
||||
|
||||
init {
|
||||
val artistName: String?
|
||||
val artistSortName: String?
|
||||
|
||||
if (raw.albumArtistName != null) {
|
||||
artistName = raw.albumArtistName
|
||||
artistSortName = raw.albumArtistSortName
|
||||
} else {
|
||||
artistName = raw.artistName
|
||||
artistSortName = raw.artistSortName
|
||||
fun _validate() {
|
||||
(checkNotNull(_album) { "Malformed song: album is null" })._validate()
|
||||
check(_genres.isNotEmpty()) { "Malformed song: genres are empty" }
|
||||
}
|
||||
|
||||
_rawAlbum =
|
||||
Album.Raw(
|
||||
mediaStoreId = raw.albumMediaStoreId,
|
||||
name = raw.albumName,
|
||||
sortName = raw.albumSortName,
|
||||
date = raw.date,
|
||||
releaseType = raw.albumReleaseType,
|
||||
artistName,
|
||||
artistSortName)
|
||||
init {
|
||||
// Generally, we calculate UIDs at the end since everything will definitely be initialized
|
||||
// by now.
|
||||
uid =
|
||||
UID.hashed(this::class) {
|
||||
update(rawName.lowercase())
|
||||
update(_rawAlbum.name.lowercase())
|
||||
update(_rawAlbum.date)
|
||||
|
||||
update(raw.artistName)
|
||||
update(raw.albumArtistName)
|
||||
|
||||
update(track)
|
||||
update(disc)
|
||||
|
||||
update(durationMs.msToSecs())
|
||||
}
|
||||
}
|
||||
|
||||
data class Raw(
|
||||
|
@ -225,43 +323,26 @@ data class Song(private val raw: Raw) : Music() {
|
|||
}
|
||||
|
||||
/** The data object for an album. */
|
||||
data class Album(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
init {
|
||||
for (song in songs) {
|
||||
song._link(this)
|
||||
}
|
||||
}
|
||||
class Album(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
override val uid: UID
|
||||
|
||||
override val id: Long
|
||||
get() {
|
||||
var result = rawName.toMusicId()
|
||||
result = 31 * result + artist.rawName.toMusicId()
|
||||
result = 31 * result + (date?.year ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No name" }
|
||||
override val rawName = raw.name
|
||||
|
||||
override val rawSortName = raw.sortName
|
||||
|
||||
override fun resolveName(context: Context) = rawName
|
||||
|
||||
/**
|
||||
* The album cover URI for this album. Usually low quality, so using Coil is recommended
|
||||
* instead.
|
||||
*/
|
||||
val coverUri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.albumCoverUri
|
||||
|
||||
/** The latest date this album was released. */
|
||||
val date = raw.date
|
||||
|
||||
/** The release type of this album, such as "EP". Defaults to "Album". */
|
||||
val releaseType = raw.releaseType ?: ReleaseType.Album(null)
|
||||
|
||||
private var _artist: Artist? = null
|
||||
/** The parent artist of this album. */
|
||||
val artist: Artist
|
||||
get() = unlikelyToBeNull(_artist)
|
||||
/**
|
||||
* The album cover URI for this album. Usually low quality, so using Coil is recommended
|
||||
* instead.
|
||||
*/
|
||||
val coverUri = raw.mediaStoreId.albumCoverUri
|
||||
|
||||
/** The earliest date a song in this album was added. */
|
||||
val dateAdded = songs.minOf { it.dateAdded }
|
||||
|
@ -269,34 +350,50 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
|
|||
/** The total duration of songs in this album, in millis. */
|
||||
val durationMs = songs.sumOf { it.durationMs }
|
||||
|
||||
private var _artist: Artist? = null
|
||||
/** The parent artist of this album. */
|
||||
val artist: Artist
|
||||
get() = unlikelyToBeNull(_artist)
|
||||
|
||||
// --- INTERNAL FIELDS ---
|
||||
|
||||
val _rawArtist: Artist.Raw
|
||||
get() = Artist.Raw(name = raw.artistName, sortName = raw.artistSortName)
|
||||
|
||||
val _isMissingArtist: Boolean
|
||||
get() = _artist == null
|
||||
val _rawArtist = raw.rawArtist
|
||||
|
||||
fun _link(artist: Artist) {
|
||||
_artist = artist
|
||||
}
|
||||
|
||||
data class Raw(
|
||||
val mediaStoreId: Long?,
|
||||
val name: String?,
|
||||
fun _validate() {
|
||||
checkNotNull(_artist) { "Invalid album: Artist is null " }
|
||||
}
|
||||
|
||||
init {
|
||||
uid =
|
||||
UID.hashed(this::class) {
|
||||
update(rawName)
|
||||
update(_rawArtist.name)
|
||||
update(date)
|
||||
}
|
||||
|
||||
for (song in songs) {
|
||||
song._link(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Raw(
|
||||
val mediaStoreId: Long,
|
||||
val name: String,
|
||||
val sortName: String?,
|
||||
val date: Date?,
|
||||
val releaseType: ReleaseType?,
|
||||
val artistName: String?,
|
||||
val artistSortName: String?,
|
||||
val rawArtist: Artist.Raw
|
||||
) {
|
||||
val groupingId: Long
|
||||
private val hashCode = 31 * name.lowercase().hashCode() + rawArtist.hashCode()
|
||||
|
||||
init {
|
||||
var groupingIdResult = artistName.toMusicId()
|
||||
groupingIdResult = 31 * groupingIdResult + name.toMusicId()
|
||||
groupingId = groupingIdResult
|
||||
}
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Raw && name.equals(other.name, true) && rawArtist == other.rawArtist
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,19 +401,12 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
|
|||
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
|
||||
* artist or artist field, not the individual performers of an artist.
|
||||
*/
|
||||
data class Artist(
|
||||
private val raw: Raw,
|
||||
class Artist(
|
||||
raw: Raw,
|
||||
/** The albums of this artist. */
|
||||
val albums: List<Album>
|
||||
) : MusicParent() {
|
||||
init {
|
||||
for (album in albums) {
|
||||
album._link(this)
|
||||
}
|
||||
}
|
||||
|
||||
override val id: Long
|
||||
get() = rawName.toMusicId()
|
||||
override val uid: UID
|
||||
|
||||
override val rawName = raw.name
|
||||
|
||||
|
@ -329,59 +419,93 @@ data class Artist(
|
|||
/** The total duration of songs in this artist, in millis. */
|
||||
val durationMs = songs.sumOf { it.durationMs }
|
||||
|
||||
data class Raw(val name: String?, val sortName: String?) {
|
||||
val groupingId = name.toMusicId()
|
||||
init {
|
||||
uid = UID.hashed(this::class) { update(rawName) }
|
||||
|
||||
for (album in albums) {
|
||||
album._link(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Raw(val name: String?, val sortName: String?) {
|
||||
private val hashCode = name?.lowercase().hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Raw &&
|
||||
when {
|
||||
name != null && other.name != null -> name.equals(other.name, true)
|
||||
name == null && other.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The data object for a genre. */
|
||||
data class Genre(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
init {
|
||||
for (song in songs) {
|
||||
song._link(this)
|
||||
}
|
||||
}
|
||||
class Genre(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||
override val uid: UID
|
||||
|
||||
override val rawName: String?
|
||||
get() = raw.name
|
||||
override val rawName = raw.name
|
||||
|
||||
// Sort tags don't make sense on genres
|
||||
override val rawSortName: String?
|
||||
get() = rawName
|
||||
|
||||
override val id: Long
|
||||
get() = rawName.toMusicId()
|
||||
override val rawSortName = rawName
|
||||
|
||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
||||
|
||||
/** The total duration of the songs in this genre, in millis. */
|
||||
val durationMs = songs.sumOf { it.durationMs }
|
||||
|
||||
data class Raw(val name: String?) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Raw) return false
|
||||
return when {
|
||||
init {
|
||||
uid = UID.hashed(this::class) { update(rawName) }
|
||||
|
||||
for (song in songs) {
|
||||
song._link(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Raw(val name: String?) {
|
||||
private val hashCode = name?.lowercase().hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Raw &&
|
||||
when {
|
||||
name != null && other.name != null -> name.equals(other.name, true)
|
||||
name == null && other.name == null -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = name?.lowercase().hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.toMusicId(): Long {
|
||||
if (this == null) {
|
||||
// Pre-calculated hash of MediaStore.UNKNOWN_STRING
|
||||
return 54493231833456
|
||||
}
|
||||
fun MessageDigest.update(string: String?) {
|
||||
if (string == null) return
|
||||
update(string.lowercase().toByteArray())
|
||||
}
|
||||
|
||||
var result = 0L
|
||||
for (ch in lowercase()) {
|
||||
result = 31 * result + ch.lowercaseChar().code
|
||||
}
|
||||
return result
|
||||
fun MessageDigest.update(date: Date?) {
|
||||
if (date == null) return
|
||||
update(date.toString().toByteArray())
|
||||
}
|
||||
|
||||
fun MessageDigest.update(n: Int?) {
|
||||
if (n == null) return
|
||||
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||
}
|
||||
|
||||
fun MessageDigest.update(n: Long?) {
|
||||
if (n == null) return
|
||||
update(
|
||||
byteArrayOf(
|
||||
n.toByte(),
|
||||
n.shr(8).toByte(),
|
||||
n.shr(16).toByte(),
|
||||
n.shr(24).toByte(),
|
||||
n.shr(32).toByte(),
|
||||
n.shr(40).toByte(),
|
||||
n.shr(56).toByte(),
|
||||
n.shr(64).toByte()))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -75,34 +75,38 @@ class MusicStore private constructor() {
|
|||
val albums: List<Album>,
|
||||
val songs: List<Song>
|
||||
) {
|
||||
private val genreIdMap = HashMap<Long, Genre>().apply { genres.forEach { put(it.id, it) } }
|
||||
private val artistIdMap =
|
||||
HashMap<Long, Artist>().apply { artists.forEach { put(it.id, it) } }
|
||||
private val albumIdMap = HashMap<Long, Album>().apply { albums.forEach { put(it.id, it) } }
|
||||
private val songIdMap = HashMap<Long, Song>().apply { songs.forEach { put(it.id, it) } }
|
||||
private val uidMap = HashMap<Music.UID, Music>()
|
||||
|
||||
/** Find a [Song] by it's ID. Null if no song exists with that ID. */
|
||||
fun findSongById(songId: Long) = songIdMap[songId]
|
||||
init {
|
||||
for (song in songs) {
|
||||
uidMap[song.uid] = song
|
||||
}
|
||||
|
||||
/** Find a [Album] by it's ID. Null if no album exists with that ID. */
|
||||
fun findAlbumById(albumId: Long) = albumIdMap[albumId]
|
||||
for (album in albums) {
|
||||
uidMap[album.uid] = album
|
||||
}
|
||||
|
||||
/** Find a [Artist] by it's ID. Null if no artist exists with that ID. */
|
||||
fun findArtistById(artistId: Long) = artistIdMap[artistId]
|
||||
for (artist in artists) {
|
||||
uidMap[artist.uid] = artist
|
||||
}
|
||||
|
||||
/** Find a [Genre] by it's ID. Null if no genre exists with that ID. */
|
||||
fun findGenreById(genreId: Long) = genreIdMap[genreId]
|
||||
for (genre in genres) {
|
||||
uidMap[genre.uid] = genre
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T
|
||||
|
||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(song: Song) = findSongById(song.id)
|
||||
fun sanitize(song: Song) = find<Song>(song.uid)
|
||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
|
||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(album: Album) = findAlbumById(album.id)
|
||||
fun sanitize(album: Album) = find<Album>(album.uid)
|
||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(artist: Artist) = findArtistById(artist.id)
|
||||
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
|
||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(genre: Genre) = findGenreById(genre.id)
|
||||
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
|
||||
|
||||
/** Find a song for a [uri]. */
|
||||
fun findSongForUri(context: Context, uri: Uri) =
|
||||
|
|
|
@ -229,13 +229,7 @@ class Indexer {
|
|||
|
||||
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
||||
for (song in songs) {
|
||||
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
||||
error(
|
||||
"Found unlinked song: ${song.rawName} [" +
|
||||
"missing album: ${song._isMissingAlbum} " +
|
||||
"missing artist: ${song._isMissingArtist} " +
|
||||
"missing genre: ${song._isMissingGenre}]")
|
||||
}
|
||||
song._validate()
|
||||
}
|
||||
|
||||
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||
|
@ -262,7 +256,7 @@ class Indexer {
|
|||
}
|
||||
|
||||
// Deduplicate songs to prevent (most) deformed music clones
|
||||
songs = songs.distinctBy { it._distinct }.toMutableList()
|
||||
songs = songs.distinctBy { it.uid }.toMutableList()
|
||||
|
||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
|
||||
|
@ -287,7 +281,7 @@ class Indexer {
|
|||
*/
|
||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||
val albums = mutableListOf<Album>()
|
||||
val songsByAlbum = songs.groupBy { it._rawAlbum.groupingId }
|
||||
val songsByAlbum = songs.groupBy { it._rawAlbum }
|
||||
|
||||
for (entry in songsByAlbum) {
|
||||
val albumSongs = entry.value
|
||||
|
@ -296,8 +290,7 @@ class Indexer {
|
|||
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||
// weird years like "0" wont show up if there are alternatives.
|
||||
val templateSong =
|
||||
albumSongs.maxWith(
|
||||
compareBy(Sort.Mode.NullableComparator.DATE) { it._rawAlbum.date })
|
||||
albumSongs.maxWith(compareBy(Sort.Mode.NullableComparator.DATE) { entry.key.date })
|
||||
|
||||
albums.add(Album(templateSong._rawAlbum, albumSongs))
|
||||
}
|
||||
|
@ -313,12 +306,11 @@ class Indexer {
|
|||
*/
|
||||
private fun buildArtists(albums: List<Album>): List<Artist> {
|
||||
val artists = mutableListOf<Artist>()
|
||||
val albumsByArtist = albums.groupBy { it._rawArtist.groupingId }
|
||||
val albumsByArtist = albums.groupBy { it._rawArtist }
|
||||
|
||||
for (entry in albumsByArtist) {
|
||||
// The first album will suffice for template metadata.
|
||||
val templateAlbum = entry.value[0]
|
||||
artists.add(Artist(templateAlbum._rawArtist, albums = entry.value))
|
||||
artists.add(Artist(entry.key, entry.value))
|
||||
}
|
||||
|
||||
logD("Successfully built ${artists.size} artists")
|
||||
|
@ -340,7 +332,7 @@ class Indexer {
|
|||
}
|
||||
|
||||
for (entry in songsByGenre) {
|
||||
genres.add(Genre(entry.key, songs = entry.value))
|
||||
genres.add(Genre(entry.key, entry.value))
|
||||
}
|
||||
|
||||
logD("Successfully built ${genres.size} genres")
|
||||
|
|
|
@ -84,7 +84,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
|||
// User wants album gain to be used when in an album, track gain otherwise.
|
||||
ReplayGainMode.DYNAMIC ->
|
||||
playbackManager.parent is Album &&
|
||||
playbackManager.song?.album?.id == playbackManager.parent?.id
|
||||
playbackManager.song?.album == playbackManager.parent
|
||||
}
|
||||
|
||||
val resolvedGain =
|
||||
|
|
|
@ -21,11 +21,8 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -78,11 +75,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
||||
command
|
||||
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
||||
.append("${StateColumns.COLUMN_SONG_ID} LONG,")
|
||||
.append("${StateColumns.COLUMN_SONG_UID} STRING,")
|
||||
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
||||
.append("${StateColumns.COLUMN_PARENT_ID} LONG,")
|
||||
.append("${StateColumns.COLUMN_PARENT_UID} STRING,")
|
||||
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
|
||||
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
|
||||
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
|
||||
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
|
||||
|
||||
|
@ -93,8 +89,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
||||
command
|
||||
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
||||
.append("${QueueColumns.SONG_ID} INTEGER NOT NULL,")
|
||||
.append("${QueueColumns.ALBUM_ID} INTEGER NOT NULL)")
|
||||
.append("${QueueColumns.SONG_UID} STRING NOT NULL)")
|
||||
|
||||
return command
|
||||
}
|
||||
|
@ -109,17 +104,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
|
||||
// Correct the index to match up with a possibly shortened queue (file removals/changes)
|
||||
var actualIndex = rawState.index
|
||||
while (queue.getOrNull(actualIndex)?.id != rawState.songId && actualIndex > -1) {
|
||||
while (queue.getOrNull(actualIndex)?.uid?.also { logD(it) } != rawState.songUid &&
|
||||
actualIndex > -1) {
|
||||
actualIndex--
|
||||
}
|
||||
|
||||
val parent =
|
||||
when (rawState.playbackMode) {
|
||||
PlaybackMode.ALL_SONGS -> null
|
||||
PlaybackMode.IN_ALBUM -> rawState.parentId?.let(library::findAlbumById)
|
||||
PlaybackMode.IN_ARTIST -> rawState.parentId?.let(library::findArtistById)
|
||||
PlaybackMode.IN_GENRE -> rawState.parentId?.let(library::findGenreById)
|
||||
}
|
||||
val parent = rawState.parentUid?.let { library.find<MusicParent>(it) }
|
||||
|
||||
return SavedState(
|
||||
index = actualIndex,
|
||||
|
@ -140,11 +130,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
|
||||
|
||||
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
|
||||
val playbackModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
|
||||
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
|
||||
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
|
||||
val songIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_ID)
|
||||
val parentIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_ID)
|
||||
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_UID)
|
||||
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_UID)
|
||||
|
||||
cursor.moveToFirst()
|
||||
|
||||
|
@ -154,10 +143,9 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
||||
?: RepeatMode.NONE,
|
||||
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
||||
songId = cursor.getLong(songIdIndex),
|
||||
parentId = cursor.getLongOrNull(parentIdIndex),
|
||||
playbackMode = PlaybackMode.fromInt(cursor.getInt(playbackModeIndex))
|
||||
?: PlaybackMode.ALL_SONGS)
|
||||
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
|
||||
?: return@queryAll null,
|
||||
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,9 +156,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
|
||||
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
||||
if (cursor.count == 0) return@queryAll
|
||||
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_ID)
|
||||
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
|
||||
while (cursor.moveToNext()) {
|
||||
library.findSongById(cursor.getLong(songIndex))?.let(queue::add)
|
||||
logD(cursor.getString(songIndex))
|
||||
val uid = Music.UID.fromString(cursor.getString(songIndex)) ?: continue
|
||||
val song = library.find<Song>(uid) ?: continue
|
||||
queue.add(song)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,15 +181,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
positionMs = state.positionMs,
|
||||
repeatMode = state.repeatMode,
|
||||
isShuffled = state.isShuffled,
|
||||
songId = state.queue[state.index].id,
|
||||
parentId = state.parent?.id,
|
||||
playbackMode =
|
||||
when (state.parent) {
|
||||
null -> PlaybackMode.ALL_SONGS
|
||||
is Album -> PlaybackMode.IN_ALBUM
|
||||
is Artist -> PlaybackMode.IN_ARTIST
|
||||
is Genre -> PlaybackMode.IN_GENRE
|
||||
})
|
||||
songUid = state.queue[state.index].uid,
|
||||
parentUid = state.parent?.uid)
|
||||
|
||||
writeRawState(rawState)
|
||||
writeQueue(state.queue)
|
||||
|
@ -218,11 +202,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
val stateData =
|
||||
ContentValues(10).apply {
|
||||
put(StateColumns.COLUMN_ID, 0)
|
||||
put(StateColumns.COLUMN_SONG_ID, rawState.songId)
|
||||
put(StateColumns.COLUMN_SONG_UID, rawState.songUid.toString())
|
||||
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
|
||||
put(StateColumns.COLUMN_PARENT_ID, rawState.parentId)
|
||||
put(StateColumns.COLUMN_PARENT_UID, rawState.parentUid?.toString())
|
||||
put(StateColumns.COLUMN_INDEX, rawState.index)
|
||||
put(StateColumns.COLUMN_PLAYBACK_MODE, rawState.playbackMode.intCode)
|
||||
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
|
||||
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
|
||||
}
|
||||
|
@ -255,8 +238,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
val itemData =
|
||||
ContentValues(4).apply {
|
||||
put(QueueColumns.ID, idStart + i)
|
||||
put(QueueColumns.SONG_ID, song.id)
|
||||
put(QueueColumns.ALBUM_ID, song.album.id)
|
||||
put(QueueColumns.SONG_UID, song.uid.toString())
|
||||
}
|
||||
|
||||
insert(TABLE_NAME_QUEUE, null, itemData)
|
||||
|
@ -286,31 +268,28 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
val positionMs: Long,
|
||||
val repeatMode: RepeatMode,
|
||||
val isShuffled: Boolean,
|
||||
val songId: Long,
|
||||
val parentId: Long?,
|
||||
val playbackMode: PlaybackMode
|
||||
val songUid: Music.UID,
|
||||
val parentUid: Music.UID?
|
||||
)
|
||||
|
||||
private object StateColumns {
|
||||
const val COLUMN_ID = "id"
|
||||
const val COLUMN_SONG_ID = "song"
|
||||
const val COLUMN_SONG_UID = "song_uid"
|
||||
const val COLUMN_POSITION = "position"
|
||||
const val COLUMN_PARENT_ID = "parent"
|
||||
const val COLUMN_PARENT_UID = "parent"
|
||||
const val COLUMN_INDEX = "queue_index"
|
||||
const val COLUMN_PLAYBACK_MODE = "playback_mode"
|
||||
const val COLUMN_IS_SHUFFLED = "is_shuffling"
|
||||
const val COLUMN_REPEAT_MODE = "repeat_mode"
|
||||
}
|
||||
|
||||
private object QueueColumns {
|
||||
const val ID = "id"
|
||||
const val SONG_ID = "song"
|
||||
const val ALBUM_ID = "album"
|
||||
const val SONG_UID = "song_uid"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DB_NAME = "auxio_state_database.db"
|
||||
const val DB_VERSION = 7
|
||||
const val DB_VERSION = 8
|
||||
|
||||
const val TABLE_NAME_STATE = "playback_state_table"
|
||||
const val TABLE_NAME_QUEUE = "queue_table"
|
||||
|
|
|
@ -449,7 +449,7 @@ class PlaybackStateManager private constructor() {
|
|||
// While we could just save and reload the state, we instead sanitize the state
|
||||
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
|
||||
|
||||
val oldSongId = song?.id
|
||||
val oldSongUid = song?.uid
|
||||
val oldPosition = playerState.calculateElapsedPosition()
|
||||
|
||||
parent =
|
||||
|
@ -463,7 +463,7 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
_queue = newLibrary.sanitize(_queue).toMutableList()
|
||||
|
||||
while (song?.id != oldSongId && index > -1) {
|
||||
while (song?.uid != oldSongUid && index > -1) {
|
||||
index--
|
||||
}
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
|||
// instead of loading a bitmap.
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setMediaId("Song:${song.id}")
|
||||
.setMediaId(song.uid.toString())
|
||||
.setTitle(song.resolveName(context))
|
||||
.setSubtitle(song.resolveIndividualArtistName(context))
|
||||
.setIconUri(song.album.coverUri)
|
||||
|
|
|
@ -356,7 +356,7 @@ class PlaybackService :
|
|||
}
|
||||
is InternalPlayer.Action.Open -> {
|
||||
library.findSongForUri(application, action.uri)?.let { song ->
|
||||
playbackManager.play(song, settings.libPlaybackMode, settings)
|
||||
playbackManager.play(song, null, settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,18 +90,18 @@ class SearchAdapter(private val listener: MenuItemListener) :
|
|||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Song && newItem is Song ->
|
||||
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
AlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
ArtistViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
ArtistViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
GenreViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Header && newItem is Header ->
|
||||
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,10 +170,10 @@ class SearchFragment :
|
|||
findNavController()
|
||||
.navigate(
|
||||
when (item) {
|
||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
})
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
* similarly distorted by the insets, and thus I must go further and modify the edge effect to be at
|
||||
* least somewhat clamped to the insets themselves.
|
||||
* 3. Touch events. Bottom sheets must always intercept touches in their bounds, or they will click
|
||||
* the now overlapping content view that is only inset by it and not unhidden by it.
|
||||
* the now overlapping content view that is only inset and not moved out of the way..
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
|
|
|
@ -40,7 +40,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
|||
holder.updateIndicator(
|
||||
currentItem != null &&
|
||||
item.javaClass == currentItem.javaClass &&
|
||||
item.id == currentItem.id,
|
||||
item == currentItem,
|
||||
isPlaying)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
|||
if (oldItem != null) {
|
||||
val pos =
|
||||
currentList.indexOfFirst {
|
||||
it.javaClass == oldItem.javaClass && it.id == oldItem.id
|
||||
it.javaClass == oldItem.javaClass && item == currentItem
|
||||
}
|
||||
|
||||
if (pos > -1) {
|
||||
|
@ -68,8 +68,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
|||
}
|
||||
|
||||
if (item != null) {
|
||||
val pos =
|
||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
||||
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
|
||||
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||
|
@ -85,8 +84,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
|||
this.isPlaying = isPlaying
|
||||
|
||||
if (!updatedItem && item != null) {
|
||||
val pos =
|
||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
||||
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
|
||||
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||
|
|
|
@ -23,23 +23,14 @@ import androidx.recyclerview.widget.AdapterListUpdateCallback
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
|
||||
* provided for free by the normal adapter implementations, such as certain types of diffing.
|
||||
*/
|
||||
abstract class Item {
|
||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||
abstract val id: Long
|
||||
}
|
||||
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
|
||||
interface Item
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item() {
|
||||
override val id: Long
|
||||
get() = string.toLong()
|
||||
}
|
||||
) : Item
|
||||
|
||||
/** An interface for detecting if an item has been clicked once. */
|
||||
interface ItemClickListener {
|
||||
|
@ -165,8 +156,5 @@ class SyncListDiffer<T>(
|
|||
* [areContentsTheSame] any object that is derived from [Item].
|
||||
*/
|
||||
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
if (oldItem.javaClass != newItem.javaClass) return false
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.individualArtistRawName == oldItem.individualArtistRawName
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ private constructor(
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
|
@ -145,7 +145,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Artist>() {
|
||||
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
|
||||
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.albums.size == newItem.albums.size &&
|
||||
oldItem.songs.size == newItem.songs.size
|
||||
|
@ -187,7 +187,7 @@ private constructor(
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
|
|||
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Header>() {
|
||||
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
Now, it would be quite cool if we could implement shared element transitions
|
||||
between elements in this navigation web. Sadly though, the shared element transition
|
||||
system is filled with so many bugs and visual errors to make this a terrible idea.
|
||||
Just use the boring, yet sane and functional fade transitions.
|
||||
Just use the boring, yet sane and functional axis transitions.
|
||||
-->
|
||||
|
||||
<fragment
|
||||
|
@ -17,8 +17,8 @@
|
|||
android:label="ArtistDetailFragment"
|
||||
tools:layout="@layout/fragment_detail">
|
||||
<argument
|
||||
android:name="artistId"
|
||||
app:argType="long" />
|
||||
android:name="artistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
<action
|
||||
android:id="@+id/action_show_artist"
|
||||
app:destination="@id/artist_detail_fragment" />
|
||||
|
@ -32,8 +32,8 @@
|
|||
android:label="AlbumDetailFragment"
|
||||
tools:layout="@layout/fragment_detail">
|
||||
<argument
|
||||
android:name="albumId"
|
||||
app:argType="long" />
|
||||
android:name="albumUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
<action
|
||||
android:id="@+id/action_show_artist"
|
||||
app:destination="@id/artist_detail_fragment" />
|
||||
|
@ -47,8 +47,8 @@
|
|||
android:label="GenreDetailFragment"
|
||||
tools:layout="@layout/fragment_detail">
|
||||
<argument
|
||||
android:name="genreId"
|
||||
app:argType="long" />
|
||||
android:name="genreUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
<action
|
||||
android:id="@+id/action_show_artist"
|
||||
app:destination="@id/artist_detail_fragment" />
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
android:label="song_detail_dialog"
|
||||
tools:layout="@layout/dialog_song_detail">
|
||||
<argument
|
||||
android:name="songId"
|
||||
app:argType="long" />
|
||||
android:name="songUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
</navigation>
|
Loading…
Reference in a new issue