music: add support for album date ranges
Add support for albums to have a range of dates. Often compilation albums will have Songs released in different months or years, so it makes some sense to show a date range rather than just the ealiest date. The only point at which the earliest date is still shown is in the home view's popup, as maxiumum dates in a date range are not sorted by, and so showing it doesn't make sense.
This commit is contained in:
parent
4533251efd
commit
cf6e7a5f0d
8 changed files with 79 additions and 31 deletions
|
@ -2,6 +2,9 @@
|
|||
|
||||
## dev
|
||||
|
||||
#### What's New
|
||||
- Added support for album date ranges (ex. 2010 - 2013)
|
||||
|
||||
#### What's Improved
|
||||
- Formalized whitespace handling
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
// Date, song count, and duration map to the info text
|
||||
binding.detailInfo.apply {
|
||||
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
||||
val date = album.date?.resolveDate(context) ?: context.getString(R.string.def_date)
|
||||
val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date)
|
||||
val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size)
|
||||
val duration = album.durationMs.formatDurationMs(true)
|
||||
text = context.getString(R.string.fmt_three, date, songCount, duration)
|
||||
|
@ -170,7 +170,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.areArtistContentsTheSame(newItem) &&
|
||||
oldItem.date == newItem.date &&
|
||||
oldItem.dates == newItem.dates &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationMs == newItem.durationMs &&
|
||||
oldItem.type == newItem.type
|
||||
|
|
|
@ -189,7 +189,8 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
|||
binding.parentName.text = album.resolveName(binding.context)
|
||||
binding.parentInfo.text =
|
||||
// Fall back to a friendlier "No date" text if the album doesn't have date information
|
||||
album.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||
album.dates?.resolveDate(binding.context)
|
||||
?: binding.context.getString(R.string.def_date)
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
|
@ -217,7 +218,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
|||
val DIFF_CALLBACK =
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName && oldItem.date == newItem.date
|
||||
oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,8 +94,8 @@ class AlbumListFragment :
|
|||
is Sort.Mode.ByArtist ->
|
||||
album.artists[0].collationKey?.run { sourceString.first().uppercase() }
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext())
|
||||
// Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd)
|
||||
is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) }
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false)
|
||||
|
|
|
@ -103,7 +103,7 @@ class SongListFragment :
|
|||
song.album.collationKey?.run { sourceString.first().uppercase() }
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext())
|
||||
is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext())
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.Mode.ByDuration -> song.durationMs.formatDurationMs(false)
|
||||
|
|
|
@ -610,11 +610,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
override val collationKey = makeCollationKeyImpl()
|
||||
override fun resolveName(context: Context) = rawName
|
||||
|
||||
/**
|
||||
* The earliest [Date] this album was released. Will be null if no valid date was present in the
|
||||
* metadata of any [Song]
|
||||
*/
|
||||
val date: Date? // TODO: Date ranges?
|
||||
/** The [DateRange] that [Song]s in the [Album] were released. */
|
||||
val dates: DateRange? = DateRange.from(songs.mapNotNull { it.date })
|
||||
|
||||
/**
|
||||
* The [Type] of this album, signifying the type of release it actually is. Defaults to
|
||||
|
@ -634,31 +631,18 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
val dateAdded: Long
|
||||
|
||||
init {
|
||||
var earliestDate: Date? = null
|
||||
var totalDuration: Long = 0
|
||||
var earliestDateAdded: Long = Long.MAX_VALUE
|
||||
|
||||
// Do linking and value generation in the same loop for efficiency.
|
||||
for (song in songs) {
|
||||
song._link(this)
|
||||
|
||||
if (song.date != null) {
|
||||
// Since we can't really assign a maximum value for dates, we instead
|
||||
// just check if the current earliest date doesn't exist and fill it
|
||||
// in with the current song if that's the case.
|
||||
if (earliestDate == null || song.date < earliestDate) {
|
||||
earliestDate = song.date
|
||||
}
|
||||
}
|
||||
|
||||
if (song.dateAdded < earliestDateAdded) {
|
||||
earliestDateAdded = song.dateAdded
|
||||
}
|
||||
|
||||
totalDuration += song.durationMs
|
||||
}
|
||||
|
||||
date = earliestDate
|
||||
durationMs = totalDuration
|
||||
dateAdded = earliestDateAdded
|
||||
}
|
||||
|
@ -1385,6 +1369,63 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A range of [Date]s. This is used in contexts where the [Date] of an item is derived from several
|
||||
* sub-items and thus can have a "range" of release dates.
|
||||
* @param min The earliest [Date] in the range.
|
||||
* @param max The latest [Date] in the range. May be the same as [min].
|
||||
* @author Alexander Capehart
|
||||
*/
|
||||
class DateRange private constructor(val min: Date, val max: Date) : Comparable<DateRange> {
|
||||
|
||||
/**
|
||||
* Resolve this instance into a human-readable date range.
|
||||
* @param context [Context] required to get human-readable names.
|
||||
* @return If the date has a maximum value, then a `min - max` formatted string will be returned
|
||||
* with the formatted [Date]s of the minimum and maximum dates respectively. Otherwise, the
|
||||
* formatted name of the minimum [Date] will be returned.
|
||||
*/
|
||||
fun resolveDate(context: Context) =
|
||||
if (min != max) {
|
||||
context.getString(
|
||||
R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context))
|
||||
} else {
|
||||
min.resolveDate(context)
|
||||
}
|
||||
|
||||
override fun compareTo(other: DateRange): Int {
|
||||
return min.compareTo(other.min)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a [DateRange] from the given list of [Date]s.
|
||||
* @param dates The [Date]s to use.
|
||||
* @return A [DateRange] based on the minimum and maximum [Date]s from [dates], or a
|
||||
* [DateRange] with a single minimum. If no [Date]s were given, null is returned.
|
||||
*/
|
||||
fun from(dates: List<Date>): DateRange? {
|
||||
if (dates.isEmpty()) {
|
||||
// Nothing to do.
|
||||
return null
|
||||
}
|
||||
// Simultaneously find the minimum and maximum values in the given range.
|
||||
// If this list has only one item, then that one date is the minimum and maximum.
|
||||
var min = dates.first()
|
||||
var max = min
|
||||
for (i in 1..dates.lastIndex) {
|
||||
if (dates[i] < min) {
|
||||
min = dates[i]
|
||||
}
|
||||
if (dates[i] > max) {
|
||||
max = dates[i]
|
||||
}
|
||||
}
|
||||
return DateRange(min, max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- MUSIC UID CREATION UTILITIES ---
|
||||
|
||||
/**
|
||||
|
|
|
@ -232,7 +232,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists },
|
||||
compareByDescending(NullableComparator.DATE) { it.album.date },
|
||||
compareByDescending(NullableComparator.DATE_RANGE) { it.album.dates },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
|
@ -241,14 +241,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists },
|
||||
compareByDescending(NullableComparator.DATE) { it.date },
|
||||
compareByDescending(NullableComparator.DATE_RANGE) { it.dates },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by the [Date] of an item. Only available for [Song] and [Album].
|
||||
* @see Song.date
|
||||
* @see Album.date
|
||||
* @see Album.dates
|
||||
*/
|
||||
object ByDate : Mode() {
|
||||
override val intCode: Int
|
||||
|
@ -259,7 +259,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
|
||||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, NullableComparator.DATE) { it.album.date },
|
||||
compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.album.dates },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
|
@ -267,7 +267,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
|
||||
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, NullableComparator.DATE) { it.date },
|
||||
compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.dates },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
}
|
||||
|
||||
|
@ -366,7 +366,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
/**
|
||||
* Sort by the date an item was added. Only supported by [Song]s and [Album]s.
|
||||
* @see Song.dateAdded
|
||||
* @see Album.date
|
||||
* @see Album.dates
|
||||
*/
|
||||
object ByDateAdded : Mode() {
|
||||
override val intCode: Int
|
||||
|
@ -545,6 +545,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
val LONG = NullableComparator<Long>()
|
||||
/** A re-usable instance configured for [Date]s. */
|
||||
val DATE = NullableComparator<Date>()
|
||||
/** A re-usable instance configured for [DateRange]s. */
|
||||
val DATE_RANGE = NullableComparator<DateRange>()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<string name="fmt_two" translatable="false">%1$s • %2$s</string>
|
||||
<string name="fmt_three" translatable="false">%1$s • %2$s • %3$s</string>
|
||||
<string name="fmt_number" translatable="false">%d</string>
|
||||
<string name="fmt_date_range" translatable="false">%s - %s</string>
|
||||
<string name="fmt_path">%1$s/%2$s</string>
|
||||
|
||||
<!-- Codec Namespace | Format names -->
|
||||
|
|
Loading…
Reference in a new issue