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:
parent
04f254f91b
commit
c522af546c
13 changed files with 132 additions and 28 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue