Add shuffling

Add basic shuffling to PlaybackFragment.
This commit is contained in:
OxygenCobalt 2020-10-15 11:31:31 -06:00
parent 339100e436
commit c422071e93
15 changed files with 190 additions and 78 deletions

View file

@ -8,6 +8,7 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
// TODO: Add ability to highlight currently playing songs
class DetailAlbumAdapter(
private val doOnClick: (Album) -> Unit
) : ListAdapter<Album, DetailAlbumAdapter.ViewHolder>(DiffCallback()) {

View file

@ -11,7 +11,6 @@ import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
class DetailSongAdapter(
private val doOnClick: (Song) -> Unit
) : ListAdapter<Song, DetailSongAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemAlbumSongBinding.inflate(LayoutInflater.from(parent.context))

View file

@ -22,7 +22,9 @@ class MusicStore private constructor() {
private var mSongs = listOf<Song>()
val songs: List<Song> get() = mSongs
suspend fun load(app: Application): MusicLoaderResponse {
// Load/Sort the entire library.
// ONLY CALL THIS FROM AN IO THREAD.
fun load(app: Application): MusicLoaderResponse {
Log.i(this::class.simpleName, "Starting initial music load...")
val start = System.currentTimeMillis()

View file

@ -25,7 +25,6 @@ class CompactPlaybackFragment : Fragment() {
): View? {
val binding = FragmentCompactPlaybackBinding.inflate(inflater)
// FIXME: Stop these icons from self-animating on creation.
val iconPauseToPlay = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_pause_to_play
) as AnimatedVectorDrawable

View file

@ -23,9 +23,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
// TODO: Implement media controls
// TODO: Make exit icon bigger
// TODO: Implement nav to artists/albums
// TODO: Possibly implement a trackbar with a spectrum shown as well.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -98,6 +96,15 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
}
playbackModel.isShuffling.observe(viewLifecycleOwner) {
// Highlight the shuffle button if Playback is shuffled, and revert it if not.
if (it) {
binding.playbackShuffle.imageTintList = accentColor
} else {
binding.playbackShuffle.imageTintList = controlColor
}
}
playbackModel.isSeeking.observe(viewLifecycleOwner) {
// Highlight the current duration if the user is seeking, and revert it if not.
if (it) {

View file

@ -7,12 +7,4 @@ package org.oxycblt.auxio.playback
// IN_ALBUM -> Play from the songs of the album
enum class PlaybackMode {
IN_ARTIST, IN_ALBUM, ALL_SONGS;
// Make a slice of all the values that this ShowMode covers.
// ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
fun getChildren(): List<PlaybackMode> {
val vals = values()
return vals.slice(vals.indexOf(this) until vals.size)
}
}

View file

@ -8,10 +8,11 @@ import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import kotlin.random.Random
import kotlin.random.Random.Default.nextLong
// TODO: Implement media controls
// TODO: Implement persistence
@ -39,6 +40,11 @@ class PlaybackViewModel : ViewModel() {
private val mIsPlaying = MutableLiveData(false)
val isPlaying: LiveData<Boolean> get() = mIsPlaying
private val mIsShuffling = MutableLiveData(false)
val isShuffling: LiveData<Boolean> get() = mIsShuffling
private val mShuffleSeed = MutableLiveData(-1L)
private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> get() = mIsSeeking
@ -57,6 +63,8 @@ class PlaybackViewModel : ViewModel() {
val musicStore = MusicStore.getInstance()
mCurrentMode.value = mode
updatePlayback(song)
mQueue.value = when (mode) {
@ -65,7 +73,18 @@ class PlaybackViewModel : ViewModel() {
PlaybackMode.IN_ALBUM -> song.album.songs
}
mCurrentMode.value = mode
mCurrentParent.value = when (mode) {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_ALBUM -> song.album
}
if (isShuffling.value!!) {
genShuffle(true)
} else {
resetShuffle()
}
mCurrentIndex.value = mQueue.value!!.indexOf(song)
}
@ -79,6 +98,7 @@ class PlaybackViewModel : ViewModel() {
mQueue.value = songs
mCurrentIndex.value = 0
mCurrentParent.value = album
mIsShuffling.value = isShuffled
mCurrentMode.value = PlaybackMode.IN_ALBUM
}
@ -92,71 +112,49 @@ class PlaybackViewModel : ViewModel() {
mQueue.value = songs
mCurrentIndex.value = 0
mCurrentParent.value = artist
mIsShuffling.value = isShuffled
mCurrentMode.value = PlaybackMode.IN_ARTIST
}
fun play(genre: Genre, isShuffled: Boolean) {
Log.d(this::class.simpleName, "Playing genre ${genre.name}")
val songs = orderSongsInGenre(genre)
updatePlayback(songs[0])
mQueue.value = songs
mCurrentIndex.value = 0
}
private fun updatePlayback(song: Song) {
mCurrentSong.value = song
mCurrentDuration.value = 0
if (!mIsPlaying.value!!) {
mIsPlaying.value = true
}
}
// Basic sorting functions when things are played in order
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
return album.songs.sortedBy { it.track }.toMutableList()
}
private fun orderSongsInArtist(artist: Artist): MutableList<Song> {
val final = mutableListOf<Song>()
artist.albums.sortedByDescending { it.year }.forEach {
final.addAll(it.songs.sortedBy { it.track })
}
return final
}
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
val final = mutableListOf<Song>()
genre.artists.sortedByDescending { it.name }.forEach { artist ->
artist.albums.sortedByDescending { it.year }.forEach { album ->
final.addAll(album.songs.sortedBy { it.track })
}
}
return final
}
// Invert, not directly set the playing status
fun invertPlayingStatus() {
mIsPlaying.value = !mIsPlaying.value!!
}
// Set the seeking status
fun setSeekingStatus(status: Boolean) {
mIsSeeking.value = status
}
// Update the current duration using a SeekBar progress
fun updateCurrentDurationWithProgress(progress: Int) {
mCurrentDuration.value = progress.toLong()
}
// Invert, not directly set the playing/shuffling status
// Used by the toggle buttons in playback fragment.
fun invertPlayingStatus() {
mIsPlaying.value = !mIsPlaying.value!!
}
fun invertShuffleStatus() {
mIsShuffling.value = !mIsShuffling.value!!
if (mIsShuffling.value!!) {
genShuffle(true)
} else {
resetShuffle()
}
}
// Shuffle all the songs.
fun shuffleAll() {
val musicStore = MusicStore.getInstance()
mIsShuffling.value = true
mQueue.value = musicStore.songs.toMutableList()
mCurrentMode.value = PlaybackMode.ALL_SONGS
mCurrentIndex.value = 0
genShuffle(false)
updatePlayback(mQueue.value!![0])
}
// Set the seeking status
fun setSeekingStatus(status: Boolean) {
mIsSeeking.value = status
}
fun skipNext() {
if (mCurrentIndex.value!! < mQueue.value!!.size) {
mCurrentIndex.value = mCurrentIndex.value!!.inc()
@ -172,4 +170,64 @@ class PlaybackViewModel : ViewModel() {
updatePlayback(mQueue.value!![mCurrentIndex.value!!])
}
private fun updatePlayback(song: Song) {
mCurrentSong.value = song
mCurrentDuration.value = 0
if (!mIsPlaying.value!!) {
mIsPlaying.value = true
}
}
// Generate a new shuffled queue.
private fun genShuffle(keepSong: Boolean) {
// Take a random seed and then shuffle the current queue based off of that.
// This seed will be saved in a bundle if the app closes, so that the shuffle mode
// can be restored when its started again.
val newSeed = Random.Default.nextLong()
Log.d(this::class.simpleName, "Shuffling queue with a seed of $newSeed.")
mShuffleSeed.value = newSeed
mQueue.value!!.shuffle(Random(newSeed))
mCurrentIndex.value = 0
// If specified, make the current song the first member of the queue.
if (keepSong) {
mQueue.value!!.remove(mCurrentSong.value)
mQueue.value!!.add(0, mCurrentSong.value!!)
} else {
// Otherwise, just start from the zeroth position in the queue.
mCurrentSong.value = mQueue.value!![0]
}
}
private fun resetShuffle() {
mShuffleSeed.value = -1
mQueue.value = when (mCurrentMode.value!!) {
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mCurrentParent.value as Artist)
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mCurrentParent.value as Album)
PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList()
}
mCurrentIndex.value = mQueue.value!!.indexOf(mCurrentSong.value)
}
// Basic sorting functions when things are played in order
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
return album.songs.sortedBy { it.track }.toMutableList()
}
private fun orderSongsInArtist(artist: Artist): MutableList<Song> {
val final = mutableListOf<Song>()
artist.albums.sortedByDescending { it.year }.forEach {
final.addAll(it.songs.sortedBy { it.track })
}
return final
}
}

View file

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackMode
@ -30,6 +31,13 @@ class SongsFragment : Fragment() {
// --- UI SETUP ---
binding.songToolbar.setOnMenuItemClickListener {
if (it.itemId == R.id.action_shuffle) {
playbackModel.shuffleAll()
}
true
}
binding.songRecycler.apply {
adapter = SongAdapter(musicStore.songs) {
playbackModel.update(it, PlaybackMode.ALL_SONGS)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/control_color">
<path
android:fillColor="@android:color/white"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/control_color">
<path
android:fillColor="@android:color/white"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
</vector>

View file

@ -185,5 +185,19 @@
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<ImageButton
android:id="@+id/playback_shuffle"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="@dimen/size_play_pause_compact"
android:layout_height="@dimen/size_play_pause_compact"
android:layout_marginEnd="@dimen/margin_mid_large"
android:src="@drawable/ic_shuffle"
android:background="@drawable/ui_unbounded_ripple"
android:onClick="@{() -> playbackModel.invertShuffleStatus()}"
android:contentDescription="@{playbackModel.isShuffling() ? @string/description_shuffle_off : @string/description_shuffle_on"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -16,6 +16,7 @@
android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_songs"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header"
app:title="@string/title_all_songs" />

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_shuffle"
android:icon="@drawable/ic_shuffle_small"
android:title="@string/label_shuffle"
app:showAsAction="always" />
</menu>

View file

@ -23,6 +23,7 @@
<string name="label_sort_none">Default</string>
<string name="label_sort_alpha_down">A-Z</string>
<string name="label_sort_alpha_up">Z-A</string>
<string name="label_shuffle">Shuffle</string>
<!-- Hint Namespace | EditText Hints -->
<string name="hint_search_library">Search Library…</string>
@ -41,6 +42,8 @@
<string name="description_pause">Pause</string>
<string name="description_skip_next">Skip to next song</string>
<string name="description_skip_prev">Skip to last song</string>
<string name="description_shuffle_on">Turn shuffle on</string>
<string name="description_shuffle_off">Turn shuffle off</string>
<!-- Placeholder Namespace | Placeholder values -->
<string name="placeholder_genre">Unknown Genre</string>

View file

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO: Try to make some of these styles better -->
<!-- Base theme -->
<style name="Theme.Base" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@color/background</item>
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:fontFamily">@font/inter</item>
<item name="android:textCursorDrawable">@drawable/ui_cursor</item>
<item name="actionBarPopupTheme">@style/AppThemeOverlay.Popup</item>
</style>
<!-- Toolbar Themes -->
<style name="Toolbar.Style" parent="ThemeOverlay.MaterialComponents.ActionBar">
<item name="android:searchViewStyle">@style/Widget.AppCompat.SearchView</item>
</style>
@ -19,6 +18,7 @@
<item name="android:textColor">?android:attr/colorPrimary</item>
</style>
<!-- Header Themes -->
<style name="DetailHeader">
<item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
<item name="android:textColor">?android:attr/colorPrimary</item>
@ -30,12 +30,9 @@
<item name="android:fontFamily">@font/inter_semibold</item>
</style>
<!-- Custom popup theme -->
<style name="AppThemeOverlay.Popup" parent="ThemeOverlay.AppCompat.DayNight">
<item name="android:colorBackground">@color/background</item>
<item name="colorControlHighlight">@color/selection_color</item>
</style>
<style name="Theme.BottomSheet" parent="Theme.Design.BottomSheetDialog">
<item name="android:colorPrimary">?android:attr/colorPrimary</item>
</style>
</resources>