detail: add support for disc subtittles
Add support for disc subtitle information This allows disc groups to become named, which is useful for certain multi-part albums. Resolves #331.
This commit is contained in:
parent
82a64b5e17
commit
26f0fb7aba
12 changed files with 168 additions and 70 deletions
|
@ -2,10 +2,14 @@
|
|||
|
||||
## dev
|
||||
|
||||
#### What's New
|
||||
- Added support for disc subtitles
|
||||
|
||||
#### What's Improved
|
||||
- Auxio will now accept zeroed track/disc numbers in the presence of non-zero total
|
||||
track/disc fields.
|
||||
|
||||
|
||||
## 3.0.2
|
||||
|
||||
#### What's New
|
||||
|
|
|
@ -29,13 +29,6 @@ import org.oxycblt.auxio.music.storage.MimeType
|
|||
*/
|
||||
data class SortHeader(@StringRes val titleRes: Int) : Item
|
||||
|
||||
/**
|
||||
* A header variation that delimits between disc groups.
|
||||
* @param disc The disc number to be displayed on the header.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
data class DiscHeader(val disc: Int) : Item
|
||||
|
||||
/**
|
||||
* The properties of a [Song]'s file.
|
||||
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.music.library.Library
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
import org.oxycblt.auxio.music.storage.MimeType
|
||||
import org.oxycblt.auxio.music.tags.Disc
|
||||
import org.oxycblt.auxio.music.tags.ReleaseType
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
@ -323,11 +324,11 @@ class DetailViewModel(application: Application) :
|
|||
// songs up by disc and then delimit the groups by a disc header.
|
||||
val songs = albumSongSort.songs(album.songs)
|
||||
// Songs without disc tags become part of Disc 1.
|
||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||
val byDisc = songs.groupBy { it.disc ?: Disc(1, null) }
|
||||
if (byDisc.size > 1) {
|
||||
logD("Album has more than one disc, interspersing headers")
|
||||
for (entry in byDisc.entries) {
|
||||
data.add(DiscHeader(entry.key))
|
||||
data.add(entry.key)
|
||||
data.addAll(entry.value)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail.recycler
|
|||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
|
@ -26,13 +27,13 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||
import org.oxycblt.auxio.detail.DiscHeader
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.SelectableListListener
|
||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.tags.Disc
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
|
@ -60,7 +61,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
when (getItem(position)) {
|
||||
// Support the Album header, sub-headers for each disc, and special album songs.
|
||||
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
||||
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
|
||||
is Disc -> DiscViewHolder.VIEW_TYPE
|
||||
is Song -> AlbumSongViewHolder.VIEW_TYPE
|
||||
else -> super.getItemViewType(position)
|
||||
}
|
||||
|
@ -68,7 +69,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
when (viewType) {
|
||||
AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent)
|
||||
DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent)
|
||||
DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
|
||||
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
super.onBindViewHolder(holder, position)
|
||||
when (val item = getItem(position)) {
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
||||
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
|
||||
is Disc -> (holder as DiscViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
}
|
||||
// The album and disc headers should be full-width in all configurations.
|
||||
val item = getItem(position)
|
||||
return item is Album || item is DiscHeader
|
||||
return item is Album || item is Disc
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
@ -99,8 +100,8 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
return when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is DiscHeader && newItem is DiscHeader ->
|
||||
DiscHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Disc && newItem is Disc ->
|
||||
DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
oldItem is Song && newItem is Song ->
|
||||
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
|
||||
|
@ -182,18 +183,22 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use
|
||||
* [from] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from]
|
||||
* to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
||||
private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
* @param discHeader The new [DiscHeader] to bind.
|
||||
* @param disc The new [disc] to bind.
|
||||
*/
|
||||
fun bind(discHeader: DiscHeader) {
|
||||
binding.discNo.text = binding.context.getString(R.string.fmt_disc_no, discHeader.disc)
|
||||
fun bind(disc: Disc) {
|
||||
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
|
||||
binding.discName.apply {
|
||||
text = disc.name
|
||||
isGone = disc.name == null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -206,13 +211,13 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
* @return A new instance.
|
||||
*/
|
||||
fun from(parent: View) =
|
||||
DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
|
||||
DiscViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
|
||||
|
||||
/** A comparator that can be used with DiffUtil. */
|
||||
val DIFF_CALLBACK =
|
||||
object : SimpleDiffCallback<DiscHeader>() {
|
||||
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
|
||||
oldItem.disc == newItem.disc
|
||||
object : SimpleDiffCallback<Disc>() {
|
||||
override fun areContentsTheSame(oldItem: Disc, newItem: Disc) =
|
||||
oldItem.number == newItem.number && oldItem.name == newItem.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.parsing.parseId3GenreNames
|
|||
import org.oxycblt.auxio.music.parsing.parseMultiValue
|
||||
import org.oxycblt.auxio.music.storage.*
|
||||
import org.oxycblt.auxio.music.tags.Date
|
||||
import org.oxycblt.auxio.music.tags.Disc
|
||||
import org.oxycblt.auxio.music.tags.ReleaseType
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -340,8 +341,8 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
|
|||
/** The track number. Will be null if no valid track number was present in the metadata. */
|
||||
val track = raw.track
|
||||
|
||||
/** The disc number. Will be null if no valid disc number was present in the metadata. */
|
||||
val disc = raw.disc
|
||||
/** The [Disc] number. Will be null if no valid disc number was present in the metadata. */
|
||||
val disc = raw.disc?.let { Disc(it, raw.subtitle) }
|
||||
|
||||
/** The release [Date]. Will be null if no valid date was present in the metadata. */
|
||||
val date = raw.date
|
||||
|
@ -573,8 +574,10 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
|
|||
var sortName: String? = null,
|
||||
/** @see Song.track */
|
||||
var track: Int? = null,
|
||||
/** @see Song.disc */
|
||||
/** @see Disc.number */
|
||||
var disc: Int? = null,
|
||||
/** @See Disc.name */
|
||||
var subtitle: String? = null,
|
||||
/** @see Song.date */
|
||||
var date: Date? = null,
|
||||
/** @see Album.Raw.mediaStoreId */
|
||||
|
|
|
@ -186,6 +186,7 @@ private class CacheDatabase(context: Context) :
|
|||
append("${Columns.SORT_NAME} STRING,")
|
||||
append("${Columns.TRACK} INT,")
|
||||
append("${Columns.DISC} INT,")
|
||||
append("${Columns.SUBTITLE} STRING,")
|
||||
append("${Columns.DATE} STRING,")
|
||||
append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
|
||||
append("${Columns.ALBUM_NAME} STRING NOT NULL,")
|
||||
|
@ -243,6 +244,7 @@ private class CacheDatabase(context: Context) :
|
|||
|
||||
val trackIndex = cursor.getColumnIndexOrThrow(Columns.TRACK)
|
||||
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
|
||||
val subtitleIndex = cursor.getColumnIndex(Columns.SUBTITLE)
|
||||
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
|
||||
|
||||
val albumMusicBrainzIdIndex =
|
||||
|
@ -281,6 +283,7 @@ private class CacheDatabase(context: Context) :
|
|||
|
||||
raw.track = cursor.getIntOrNull(trackIndex)
|
||||
raw.disc = cursor.getIntOrNull(discIndex)
|
||||
raw.subtitle = cursor.getStringOrNull(subtitleIndex)
|
||||
raw.date = cursor.getStringOrNull(dateIndex)?.let(Date::from)
|
||||
|
||||
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
|
||||
|
@ -346,6 +349,7 @@ private class CacheDatabase(context: Context) :
|
|||
|
||||
put(Columns.TRACK, rawSong.track)
|
||||
put(Columns.DISC, rawSong.disc)
|
||||
put(Columns.SUBTITLE, rawSong.subtitle)
|
||||
put(Columns.DATE, rawSong.date?.toString())
|
||||
|
||||
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
||||
|
@ -414,6 +418,8 @@ private class CacheDatabase(context: Context) :
|
|||
const val TRACK = "track"
|
||||
/** @see Song.Raw.disc */
|
||||
const val DISC = "disc"
|
||||
/** @see Song.Raw.subtitle */
|
||||
const val SUBTITLE = "subtitle"
|
||||
/** @see Song.Raw.date */
|
||||
const val DATE = "date"
|
||||
/** @see Song.Raw.albumMusicBrainzId */
|
||||
|
@ -442,7 +448,7 @@ private class CacheDatabase(context: Context) :
|
|||
|
||||
companion object {
|
||||
private const val DB_NAME = "auxio_music_cache.db"
|
||||
private const val DB_VERSION = 2
|
||||
private const val DB_VERSION = 3
|
||||
private const val TABLE_RAW_SONGS = "raw_songs"
|
||||
|
||||
@Volatile private var INSTANCE: CacheDatabase? = null
|
||||
|
|
|
@ -178,15 +178,16 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
*/
|
||||
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
|
||||
// Song
|
||||
textFrames["TXXX:musicbrainz release track id"]?.let { raw.musicBrainzId = it[0] }
|
||||
textFrames["TIT2"]?.let { raw.name = it[0] }
|
||||
textFrames["TSOT"]?.let { raw.sortName = it[0] }
|
||||
textFrames["TXXX:musicbrainz release track id"]?.let { raw.musicBrainzId = it.first() }
|
||||
textFrames["TIT2"]?.let { raw.name = it.first() }
|
||||
textFrames["TSOT"]?.let { raw.sortName = it.first() }
|
||||
|
||||
// Track. Only parse out the track number and ignore the total tracks value.
|
||||
// Track.
|
||||
textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { raw.track = it }
|
||||
|
||||
// Disc. Only parse out the disc number and ignore the total discs value.
|
||||
// Disc and it's subtitle name.
|
||||
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { raw.disc = it }
|
||||
textFrames["TSST"]?.let { raw.subtitle = it.first() }
|
||||
|
||||
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
||||
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
||||
|
@ -204,9 +205,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
?.let { raw.date = it }
|
||||
|
||||
// Album
|
||||
textFrames["TXXX:musicbrainz album id"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
textFrames["TALB"]?.let { raw.albumName = it[0] }
|
||||
textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||
textFrames["TXXX:musicbrainz album id"]?.let { raw.albumMusicBrainzId = it.first() }
|
||||
textFrames["TALB"]?.let { raw.albumName = it.first() }
|
||||
textFrames["TSOA"]?.let { raw.albumSortName = it.first() }
|
||||
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
|
||||
raw.releaseTypes = it
|
||||
}
|
||||
|
@ -244,19 +245,19 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
?: textFrames["TYER"]?.run { first().toIntOrNull() } ?: return null
|
||||
|
||||
val tdat = textFrames["TDAT"]
|
||||
return if (tdat != null && tdat[0].length == 4 && tdat[0].isDigitsOnly()) {
|
||||
return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) {
|
||||
// TDAT frames consist of a 4-digit string where the first two digits are
|
||||
// the month and the last two digits are the day.
|
||||
val mm = tdat[0].substring(0..1).toInt()
|
||||
val dd = tdat[0].substring(2..3).toInt()
|
||||
val mm = tdat.first().substring(0..1).toInt()
|
||||
val dd = tdat.first().substring(2..3).toInt()
|
||||
|
||||
val time = textFrames["TIME"]
|
||||
if (time != null && time[0].length == 4 && time[0].isDigitsOnly()) {
|
||||
if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) {
|
||||
// TIME frames consist of a 4-digit string where the first two digits are
|
||||
// the hour and the last two digits are the minutes. No second value is
|
||||
// possible.
|
||||
val hh = time[0].substring(0..1).toInt()
|
||||
val mi = time[0].substring(2..3).toInt()
|
||||
val hh = time.first().substring(0..1).toInt()
|
||||
val mi = time.first().substring(2..3).toInt()
|
||||
// Able to return a full date.
|
||||
Date.from(year, mm, dd, hh, mi)
|
||||
} else {
|
||||
|
@ -275,9 +276,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
*/
|
||||
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
||||
// Song
|
||||
comments["musicbrainz_releasetrackid"]?.let { raw.musicBrainzId = it[0] }
|
||||
comments["title"]?.let { raw.name = it[0] }
|
||||
comments["titlesort"]?.let { raw.sortName = it[0] }
|
||||
comments["musicbrainz_releasetrackid"]?.let { raw.musicBrainzId = it.first() }
|
||||
comments["title"]?.let { raw.name = it.first() }
|
||||
comments["titlesort"]?.let { raw.sortName = it.first() }
|
||||
|
||||
// Track.
|
||||
parseVorbisPositionField(
|
||||
|
@ -285,11 +286,12 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
||||
?.let { raw.track = it }
|
||||
|
||||
// Disc.
|
||||
// Disc and it's subtitle name.
|
||||
parseVorbisPositionField(
|
||||
comments["discnumber"]?.first(),
|
||||
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
||||
?.let { raw.disc = it }
|
||||
comments["discsubtitle"]?.let { raw.subtitle = it.first() }
|
||||
|
||||
// Vorbis dates are less complicated, but there are still several types
|
||||
// Our hierarchy for dates is as such:
|
||||
|
@ -303,9 +305,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
?.let { raw.date = it }
|
||||
|
||||
// Album
|
||||
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
comments["album"]?.let { raw.albumName = it[0] }
|
||||
comments["albumsort"]?.let { raw.albumSortName = it[0] }
|
||||
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it.first() }
|
||||
comments["album"]?.let { raw.albumName = it.first() }
|
||||
comments["albumsort"]?.let { raw.albumSortName = it.first() }
|
||||
comments["releasetype"]?.let { raw.releaseTypes = it }
|
||||
|
||||
// Artist
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.library.Sort.Mode
|
||||
import org.oxycblt.auxio.music.tags.Date
|
||||
import org.oxycblt.auxio.music.tags.Disc
|
||||
|
||||
/**
|
||||
* A sorting method.
|
||||
|
@ -215,7 +216,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.DISC) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
@ -236,7 +237,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists },
|
||||
compareByDescending(NullableComparator.DATE_RANGE) { it.album.dates },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.DISC) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
|
||||
|
@ -263,7 +264,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
MultiComparator(
|
||||
compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.album.dates },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.DISC) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
|
||||
|
@ -342,7 +343,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
|
||||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(isAscending, NullableComparator.INT) { it.disc },
|
||||
compareByDynamic(isAscending, NullableComparator.DISC) { it.disc },
|
||||
compareBy(NullableComparator.INT) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
@ -360,7 +361,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
|
||||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareBy(NullableComparator.INT) { it.disc },
|
||||
compareBy(NullableComparator.DISC) { it.disc },
|
||||
compareByDynamic(isAscending, NullableComparator.INT) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
@ -545,6 +546,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
val INT = NullableComparator<Int>()
|
||||
/** A re-usable instance configured for [Long]s. */
|
||||
val LONG = NullableComparator<Long>()
|
||||
/** A re-usable instance configured for [Disc]s */
|
||||
val DISC = NullableComparator<Disc>()
|
||||
/** A re-usable instance configured for [Date.Range]s. */
|
||||
val DATE_RANGE = NullableComparator<Date.Range>()
|
||||
}
|
||||
|
|
31
app/src/main/java/org/oxycblt/auxio/music/tags/Disc.kt
Normal file
31
app/src/main/java/org/oxycblt/auxio/music/tags/Disc.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.tags
|
||||
|
||||
import org.oxycblt.auxio.list.Item
|
||||
|
||||
/**
|
||||
* A disc identifier for a song.
|
||||
* @param number The disc number.
|
||||
* @param name The name of the disc group, if any. Null if not present.
|
||||
*/
|
||||
class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
||||
override fun hashCode() = number.hashCode()
|
||||
override fun equals(other: Any?) = other is Disc && number == other.number
|
||||
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
||||
}
|
|
@ -300,7 +300,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong())
|
||||
}
|
||||
song.disc?.let {
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong())
|
||||
}
|
||||
song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) }
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -11,7 +11,7 @@
|
|||
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||
|
||||
<org.oxycblt.auxio.image.StyledImageView
|
||||
android:id="@+id/disc_item"
|
||||
android:id="@+id/disc_icon"
|
||||
style="@style/Widget.Auxio.Image.Small"
|
||||
android:scaleType="matrix"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -21,19 +21,30 @@
|
|||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disc_no"
|
||||
android:paddingStart="@dimen/spacing_medium"
|
||||
android:paddingEnd="@dimen/spacing_medium"
|
||||
android:paddingTop="@dimen/spacing_mid_medium"
|
||||
android:paddingBottom="@dimen/spacing_mid_medium"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
|
||||
android:id="@+id/disc_number"
|
||||
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||
android:textColor="@color/sel_selectable_text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/disc_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/disc_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Disc 1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disc_name"
|
||||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/disc_item"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Disc 16" />
|
||||
tools:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@+id/disc_icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/disc_number"
|
||||
tools:text="Part 1" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
39
app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt
Normal file
39
app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.tags
|
||||
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DiscTest {
|
||||
@Test
|
||||
fun disc_equals_correct() {
|
||||
val a = Disc(1, "Part I")
|
||||
val b = Disc(1, "Part I")
|
||||
assertTrue(a == b)
|
||||
assertTrue(a.hashCode() == b.hashCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disc_equals_inconsistentNames() {
|
||||
val a = Disc(1, "Part I")
|
||||
val b = Disc(1, null)
|
||||
assertTrue(a == b)
|
||||
assertTrue(a.hashCode() == b.hashCode())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue