diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt index 6e633919e..636b56237 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt @@ -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(DiffCallback()) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt index 151728f6b..e4a35a3f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailSongAdapter.kt @@ -11,7 +11,6 @@ import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder class DetailSongAdapter( private val doOnClick: (Song) -> Unit ) : ListAdapter(DiffCallback()) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( ItemAlbumSongBinding.inflate(LayoutInflater.from(parent.context)) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index a48d358ff..398a9df79 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -22,7 +22,9 @@ class MusicStore private constructor() { private var mSongs = listOf() val songs: List 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() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index 902cd3179..f86e11e95 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 27e77ea49..1bf3ca304 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt index c0fbddfeb..94d8e9112 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt @@ -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 { - val vals = values() - - return vals.slice(vals.indexOf(this) until vals.size) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index d5e2d8ca2..ae9ce5ca2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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 get() = mIsPlaying + private val mIsShuffling = MutableLiveData(false) + val isShuffling: LiveData get() = mIsShuffling + + private val mShuffleSeed = MutableLiveData(-1L) + private val mIsSeeking = MutableLiveData(false) val isSeeking: LiveData 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 { - return album.songs.sortedBy { it.track }.toMutableList() - } - - private fun orderSongsInArtist(artist: Artist): MutableList { - val final = mutableListOf() - - artist.albums.sortedByDescending { it.year }.forEach { - final.addAll(it.songs.sortedBy { it.track }) - } - - return final - } - - private fun orderSongsInGenre(genre: Genre): MutableList { - val final = mutableListOf() - - 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 { + return album.songs.sortedBy { it.track }.toMutableList() + } + + private fun orderSongsInArtist(artist: Artist): MutableList { + val final = mutableListOf() + + artist.albums.sortedByDescending { it.year }.forEach { + final.addAll(it.songs.sortedBy { it.track }) + } + + return final + } } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 23c429ffc..983dc1d7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -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) diff --git a/app/src/main/res/drawable/ic_shuffle.xml b/app/src/main/res/drawable/ic_shuffle.xml new file mode 100644 index 000000000..d207d1526 --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shuffle_small.xml b/app/src/main/res/drawable/ic_shuffle_small.xml new file mode 100644 index 000000000..e68fd1c25 --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle_small.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback.xml b/app/src/main/res/layout/fragment_playback.xml index 76f8599f2..c76e53546 100644 --- a/app/src/main/res/layout/fragment_playback.xml +++ b/app/src/main/res/layout/fragment_playback.xml @@ -185,5 +185,19 @@ app:layout_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_songs.xml b/app/src/main/res/layout/fragment_songs.xml index 7a010136b..c50fb623d 100644 --- a/app/src/main/res/layout/fragment_songs.xml +++ b/app/src/main/res/layout/fragment_songs.xml @@ -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" /> diff --git a/app/src/main/res/menu/menu_songs.xml b/app/src/main/res/menu/menu_songs.xml new file mode 100644 index 000000000..51b0b0685 --- /dev/null +++ b/app/src/main/res/menu/menu_songs.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c34c0365c..b669012e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Default A-Z Z-A + Shuffle Search Library… @@ -41,6 +42,8 @@ Pause Skip to next song Skip to last song + Turn shuffle on + Turn shuffle off Unknown Genre diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 431f218e1..f14780398 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,15 +1,14 @@ - + @@ -19,6 +18,7 @@ ?android:attr/colorPrimary + + - - \ No newline at end of file