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] ## dev [v2.2.3, v2.3.0, or v3.0.0]
#### What's New #### What's New
- Added disc number support
- Added ReplayGain support for below-reference volume tracks [i.e positive ReplayGain values] - 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 - 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 const val SORT_BY_ALBUM = 0xA10E
/** Sort.ByYear */ /** Sort.ByYear */
const val SORT_BY_YEAR = 0xA10F const val SORT_BY_YEAR = 0xA10F
/** Sort.ByDisc */
const val SORT_BY_DISC = 0xA114
/** Sort.ByTrack */
const val SORT_BY_TRACK = 0xA115
/** ReplayGainMode.Off */ /** ReplayGainMode.Off */
const val REPLAY_GAIN_MODE_OFF = 0xA110 const val REPLAY_GAIN_MODE_OFF = 0xA110

View file

@ -110,7 +110,11 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
anchor, anchor,
detailModel.albumSort, detailModel.albumSort,
onConfirm = { detailModel.albumSort = it }, 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() { override fun onNavigateToArtist() {

View file

@ -96,7 +96,11 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
anchor, anchor,
detailModel.artistSort, detailModel.artistSort,
onConfirm = { detailModel.artistSort = it }, 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?) { private fun handleNavigation(item: Music?) {

View file

@ -138,8 +138,20 @@ class DetailViewModel : ViewModel() {
logD("Refreshing album data") logD("Refreshing album data")
val data = mutableListOf<Item>(album) val data = mutableListOf<Item>(album)
data.add(SortHeader(id = -2, R.string.lbl_songs)) 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 _albumData.value = data
} }
} }

View file

@ -22,6 +22,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
@ -90,7 +91,11 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
} }
override fun onShowSortMenu(anchor: View) { 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?) { 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) : class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
BindingViewHolder<DiscHeader, Unit>(binding.root) { BindingViewHolder<DiscHeader, Unit>(binding.root) {
override fun bind(item: DiscHeader, listener: Unit) { 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 { companion object {

View file

@ -65,6 +65,10 @@ class SongListFragment : HomeListFragment<Song>() {
// Year -> Use Full Year // Year -> Use Full Year
is Sort.ByYear -> song.album.year?.toString() 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 --- // --- SYSTEM SETUP ---
widgetComponent = WidgetComponent(this) widgetComponent = WidgetComponent(this)
@ -150,6 +135,21 @@ class PlaybackService :
registerReceiver(systemReceiver, this) 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") logD("Service created")
} }

View file

@ -181,9 +181,17 @@ class SettingsManager private constructor(context: Context) :
/** The detail album sort mode */ /** The detail album sort mode */
var detailAlbumSort: Sort var detailAlbumSort: Sort
get() = get() {
var sort =
Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) Sort.fromIntCode(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort.ByDisc(true)
if (sort is Sort.ByName) {
sort = Sort.ByDisc(sort.isAscending)
}
return sort
}
set(value) { set(value) {
prefs.edit { prefs.edit {
putInt(KEY_DETAIL_ALBUM_SORT, value.intCode) 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.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW 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 val intCode: Int
get() = sortIntCode.shl(1) or if (isAscending) 1 else 0 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_artist -> ByArtist(isAscending)
R.id.option_sort_album -> ByAlbum(isAscending) R.id.option_sort_album -> ByAlbum(isAscending)
R.id.option_sort_year -> ByYear(isAscending) R.id.option_sort_year -> ByYear(isAscending)
R.id.option_sort_disc -> ByDisc(isAscending)
R.id.option_sort_track -> ByTrack(isAscending)
else -> null else -> null
} }
} }
@ -207,10 +261,7 @@ sealed class Sort(open val isAscending: Boolean) {
* @see songs * @see songs
*/ */
fun album(album: Album): List<Song> { fun album(album: Album): List<Song> {
return album.songs.sortedWith( return songs(album.songs)
MultiComparator(
compareByDynamic(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
} }
/** /**
@ -304,6 +355,8 @@ sealed class Sort(open val isAscending: Boolean) {
IntegerTable.SORT_BY_ARTIST -> ByArtist(ascending) IntegerTable.SORT_BY_ARTIST -> ByArtist(ascending)
IntegerTable.SORT_BY_ALBUM -> ByAlbum(ascending) IntegerTable.SORT_BY_ALBUM -> ByAlbum(ascending)
IntegerTable.SORT_BY_YEAR -> ByYear(ascending) IntegerTable.SORT_BY_YEAR -> ByYear(ascending)
IntegerTable.SORT_BY_DISC -> ByDisc(ascending)
IntegerTable.SORT_BY_TRACK -> ByTrack(ascending)
else -> null else -> null
} }
} }

View file

@ -13,6 +13,12 @@
<item <item
android:id="@+id/option_sort_year" android:id="@+id/option_sort_year"
android:title="@string/lbl_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>
<group android:checkableBehavior="all"> <group android:checkableBehavior="all">
<item <item

View file

@ -24,6 +24,8 @@
<string name="lbl_sort_artist">Artist</string> <string name="lbl_sort_artist">Artist</string>
<string name="lbl_sort_album">Album</string> <string name="lbl_sort_album">Album</string>
<string name="lbl_sort_year">Year</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_sort_asc">Ascending</string>
<string name="lbl_playback">Now Playing</string> <string name="lbl_playback">Now Playing</string>
@ -168,6 +170,7 @@
<string name="clr_grey">Grey</string> <string name="clr_grey">Grey</string>
<!-- Format Namespace | Value formatting/plurals --> <!-- 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_songs_loaded">Songs loaded: %d</string>
<string name="fmt_albums_loaded">Albums loaded: %d</string> <string name="fmt_albums_loaded">Albums loaded: %d</string>
<string name="fmt_artists_loaded">Artists loaded: %d</string> <string name="fmt_artists_loaded">Artists loaded: %d</string>