diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d5ba5f4..08afaeb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## dev +#### What's New +- Added support for album date ranges (ex. 2010 - 2013) + #### What's Improved - Formalized whitespace handling 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 8d1eb25ec..a61135a61 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 @@ -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 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 12b9b2fd4..f8c213852 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 @@ -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() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = - oldItem.rawName == newItem.rawName && oldItem.date == newItem.date + oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates } } } 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 f37309f84..01d2637fa 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 @@ -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) 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 c040761fc..1a2cc40c0 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 @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index aa424003c..299e5c60b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -610,11 +610,8 @@ class Album constructor(raw: Raw, override val songs: List) : 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) : 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) : Comparable } } +/** + * 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 { + + /** + * 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): 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 --- /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt index 7ce2248bd..4f8b9c2d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt @@ -232,7 +232,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getSongComparator(isAscending: Boolean): Comparator = 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 = 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 = 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 = 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() /** A re-usable instance configured for [Date]s. */ val DATE = NullableComparator() + /** A re-usable instance configured for [DateRange]s. */ + val DATE_RANGE = NullableComparator() } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index aca5a0088..2535c992e 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -7,6 +7,7 @@ %1$s • %2$s %1$s • %2$s • %3$s %d + %s - %s %1$s/%2$s