detail: add full disc number support

Finalize the disc number implementation within Auxio.

This is probably one of the most widely-requested features outside
of playlisting. This implementation also adds some more fine grained
sorting modes for disc numbers in particular, which actually removes
some of the quirkiness of the Sort class.

Resolves #96.
This commit is contained in:
OxygenCobalt 2022-05-19 16:40:42 -06:00
parent 04f254f91b
commit c522af546c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 132 additions and 28 deletions

View file

@ -3,6 +3,7 @@
## dev [v2.2.3, v2.3.0, or v3.0.0]
#### What's New
- Added disc number support
- Added ReplayGain support for below-reference volume tracks [i.e positive ReplayGain values]
- About screen now shows counts for multiple types of library items, alongside a total duration

View file

@ -92,6 +92,10 @@ object IntegerTable {
const val SORT_BY_ALBUM = 0xA10E
/** Sort.ByYear */
const val SORT_BY_YEAR = 0xA10F
/** Sort.ByDisc */
const val SORT_BY_DISC = 0xA114
/** Sort.ByTrack */
const val SORT_BY_TRACK = 0xA115
/** ReplayGainMode.Off */
const val REPLAY_GAIN_MODE_OFF = 0xA110

View file

@ -110,7 +110,11 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
anchor,
detailModel.albumSort,
onConfirm = { detailModel.albumSort = it },
showItem = { it == R.id.option_sort_asc })
showItem = {
it == R.id.option_sort_asc ||
it == R.id.option_sort_disc ||
it == R.id.option_sort_track
})
}
override fun onNavigateToArtist() {

View file

@ -96,7 +96,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
anchor,
detailModel.artistSort,
onConfirm = { detailModel.artistSort = it },
showItem = { id -> id != R.id.option_sort_artist })
showItem = { id ->
id != R.id.option_sort_artist &&
id != R.id.option_sort_disc &&
id != R.id.option_sort_track
})
}
private fun handleNavigation(item: Music?) {

View file

@ -138,8 +138,20 @@ class DetailViewModel : ViewModel() {
logD("Refreshing album data")
val data = mutableListOf<Item>(album)
data.add(SortHeader(id = -2, R.string.lbl_songs))
data.add(DiscHeader(id = -3, 1))
data.addAll(albumSort.album(album))
val songs = albumSort.album(album)
val byDisc = songs.groupBy { it.disc ?: 1 }
if (byDisc.size > 1) {
for (entry in byDisc.entries) {
val disc = entry.key
val discSongs = entry.value
data.add(DiscHeader(id = -2L - disc, disc))
data.addAll(discSongs)
}
} else {
data.addAll(songs)
}
_albumData.value = data
}
}

View file

@ -22,6 +22,7 @@ import android.view.MenuItem
import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
@ -90,7 +91,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
}
override fun onShowSortMenu(anchor: View) {
showSortMenu(anchor, detailModel.genreSort, onConfirm = { detailModel.genreSort = it })
showSortMenu(
anchor,
detailModel.genreSort,
onConfirm = { detailModel.genreSort = it },
showItem = { it != R.id.option_sort_disc && it != R.id.option_sort_track })
}
private fun handleNavigation(item: Music?) {

View file

@ -171,7 +171,7 @@ data class DiscHeader(override val id: Long, val disc: Int) : Item()
class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
BindingViewHolder<DiscHeader, Unit>(binding.root) {
override fun bind(item: DiscHeader, listener: Unit) {
binding.discNo.textSafe = "Disc 1"
binding.discNo.textSafe = binding.context.getString(R.string.fmt_disc_no, item.disc)
}
companion object {

View file

@ -65,6 +65,10 @@ class SongListFragment : HomeListFragment<Song>() {
// Year -> Use Full Year
is Sort.ByYear -> song.album.year?.toString()
// Unreachable state
is Sort.ByDisc,
is Sort.ByTrack -> null
}
}

View file

@ -114,21 +114,6 @@ class PlaybackService :
}
}
// --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.addCallback(this)
if (playbackManager.isInitialized) {
loadSong(playbackManager.song)
onSeek(playbackManager.positionMs)
onPlayingChanged(playbackManager.isPlaying)
onShuffledChanged(playbackManager.isShuffled)
onRepeatChanged(playbackManager.repeatMode)
}
// --- SETTINGSMANAGER SETUP ---
settingsManager.addCallback(this)
// --- SYSTEM SETUP ---
widgetComponent = WidgetComponent(this)
@ -150,6 +135,21 @@ class PlaybackService :
registerReceiver(systemReceiver, this)
}
// --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.addCallback(this)
if (playbackManager.isInitialized) {
loadSong(playbackManager.song)
onSeek(playbackManager.positionMs)
onPlayingChanged(playbackManager.isPlaying)
onShuffledChanged(playbackManager.isShuffled)
onRepeatChanged(playbackManager.repeatMode)
}
// --- SETTINGSMANAGER SETUP ---
settingsManager.addCallback(this)
logD("Service created")
}

View file

@ -181,9 +181,17 @@ class SettingsManager private constructor(context: Context) :
/** The detail album sort mode */
var detailAlbumSort: Sort
get() =
Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: Sort.ByName(true)
get() {
var sort =
Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: Sort.ByDisc(true)
if (sort is Sort.ByName) {
sort = Sort.ByDisc(sort.isAscending)
}
return sort
}
set(value) {
prefs.edit {
putInt(KEY_DETAIL_ALBUM_SORT, value.intCode)

View file

@ -25,6 +25,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/**
@ -185,6 +186,57 @@ sealed class Sort(open val isAscending: Boolean) {
}
}
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
* in a main sorting view, as it is not assigned to a particular item ID
*/
class ByDisc(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_DISC
// Not an available option, so no ID is set
override val itemId: Int
get() = R.id.option_sort_disc
override fun songs(songs: Collection<Song>): List<Song> {
logD(songs)
return songs.sortedWith(
MultiComparator(
compareByDynamic(NullableComparator()) { it.disc },
compareBy(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByDisc(newIsAscending)
}
}
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
* in a main sorting view, as it is not assigned to a particular item ID
*/
class ByTrack(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_TRACK
override val itemId: Int
get() = R.id.option_sort_track
override fun songs(songs: Collection<Song>): List<Song> {
logD(songs)
return songs.sortedWith(
MultiComparator(
compareBy(NullableComparator()) { it.disc },
compareByDynamic(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
}
override fun ascending(newIsAscending: Boolean): Sort {
return ByTrack(newIsAscending)
}
}
val intCode: Int
get() = sortIntCode.shl(1) or if (isAscending) 1 else 0
@ -198,6 +250,8 @@ sealed class Sort(open val isAscending: Boolean) {
R.id.option_sort_artist -> ByArtist(isAscending)
R.id.option_sort_album -> ByAlbum(isAscending)
R.id.option_sort_year -> ByYear(isAscending)
R.id.option_sort_disc -> ByDisc(isAscending)
R.id.option_sort_track -> ByTrack(isAscending)
else -> null
}
}
@ -207,10 +261,7 @@ sealed class Sort(open val isAscending: Boolean) {
* @see songs
*/
fun album(album: Album): List<Song> {
return album.songs.sortedWith(
MultiComparator(
compareByDynamic(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
return songs(album.songs)
}
/**
@ -304,6 +355,8 @@ sealed class Sort(open val isAscending: Boolean) {
IntegerTable.SORT_BY_ARTIST -> ByArtist(ascending)
IntegerTable.SORT_BY_ALBUM -> ByAlbum(ascending)
IntegerTable.SORT_BY_YEAR -> ByYear(ascending)
IntegerTable.SORT_BY_DISC -> ByDisc(ascending)
IntegerTable.SORT_BY_TRACK -> ByTrack(ascending)
else -> null
}
}

View file

@ -13,6 +13,12 @@
<item
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
<item
android:id="@+id/option_sort_disc"
android:title="@string/lbl_sort_disc" />
<item
android:id="@+id/option_sort_track"
android:title="@string/lbl_sort_track" />
</group>
<group android:checkableBehavior="all">
<item

View file

@ -24,6 +24,8 @@
<string name="lbl_sort_artist">Artist</string>
<string name="lbl_sort_album">Album</string>
<string name="lbl_sort_year">Year</string>
<string name="lbl_sort_disc">Disc</string>
<string name="lbl_sort_track">Track</string>
<string name="lbl_sort_asc">Ascending</string>
<string name="lbl_playback">Now Playing</string>
@ -168,6 +170,7 @@
<string name="clr_grey">Grey</string>
<!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_disc_no">Disc %d</string>
<string name="fmt_songs_loaded">Songs loaded: %d</string>
<string name="fmt_albums_loaded">Albums loaded: %d</string>
<string name="fmt_artists_loaded">Artists loaded: %d</string>