Refactor playback management
Move playback management from PlaybackViewModel to a dedicated class, update naming of playback functions heavily.
This commit is contained in:
parent
25142bba48
commit
42325c456e
14 changed files with 523 additions and 348 deletions
|
@ -92,7 +92,7 @@ class MainFragment : Fragment() {
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
||||||
playbackModel.currentSong.observe(viewLifecycleOwner) {
|
playbackModel.song.observe(viewLifecycleOwner) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.d(
|
Log.d(
|
||||||
this::class.simpleName,
|
this::class.simpleName,
|
||||||
|
|
|
@ -13,7 +13,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
|
||||||
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
|
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
import org.oxycblt.auxio.theme.disable
|
import org.oxycblt.auxio.theme.disable
|
||||||
|
@ -47,7 +47,7 @@ class AlbumDetailFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val songAdapter = DetailSongAdapter {
|
val songAdapter = DetailSongAdapter {
|
||||||
playbackModel.update(it, PlaybackMode.IN_ALBUM)
|
playbackModel.playSong(it, PlaybackMode.IN_ALBUM)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
@ -64,11 +64,13 @@ class AlbumDetailFragment : Fragment() {
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.action_shuffle -> playbackModel.play(
|
R.id.action_shuffle -> playbackModel.playAlbum(
|
||||||
detailModel.currentAlbum.value!!,
|
detailModel.currentAlbum.value!!,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
R.id.action_play -> playbackModel.play(detailModel.currentAlbum.value!!, false)
|
R.id.action_play -> playbackModel.playAlbum(
|
||||||
|
detailModel.currentAlbum.value!!, false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -67,11 +67,13 @@ class ArtistDetailFragment : Fragment() {
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.action_shuffle -> playbackModel.play(
|
R.id.action_shuffle -> playbackModel.playArtist(
|
||||||
detailModel.currentArtist.value!!,
|
detailModel.currentArtist.value!!,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
R.id.action_play -> playbackModel.play(detailModel.currentArtist.value!!, false)
|
R.id.action_play -> playbackModel.playArtist(
|
||||||
|
detailModel.currentArtist.value!!, false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -67,11 +67,13 @@ class GenreDetailFragment : Fragment() {
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.action_shuffle -> playbackModel.play(
|
R.id.action_shuffle -> playbackModel.playGenre(
|
||||||
detailModel.currentGenre.value!!,
|
detailModel.currentGenre.value!!,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
R.id.action_play -> playbackModel.play(detailModel.currentGenre.value!!, false)
|
R.id.action_play -> playbackModel.playGenre(
|
||||||
|
detailModel.currentGenre.value!!, false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -25,7 +25,7 @@ import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.theme.applyColor
|
import org.oxycblt.auxio.theme.applyColor
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
|
@ -119,8 +119,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Change LibraryAdapter to a ListAdapter
|
|
||||||
// [If there's a way to preserve scroll position properly]
|
|
||||||
binding.libraryRecycler.apply {
|
binding.libraryRecycler.apply {
|
||||||
adapter = libraryAdapter
|
adapter = libraryAdapter
|
||||||
|
|
||||||
|
@ -183,7 +181,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
// If the item is a song [That was selected through search], then update the playback
|
// If the item is a song [That was selected through search], then update the playback
|
||||||
// to that song instead of doing any navigation
|
// to that song instead of doing any navigation
|
||||||
if (baseModel is Song) {
|
if (baseModel is Song) {
|
||||||
playbackModel.update(baseModel, PlaybackMode.ALL_SONGS)
|
playbackModel.playSong(baseModel, PlaybackMode.ALL_SONGS)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@ import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
|
||||||
class CompactPlaybackFragment : Fragment() {
|
class CompactPlaybackFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels {
|
||||||
|
PlaybackViewModel.Factory(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -50,7 +52,7 @@ class CompactPlaybackFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.currentSong.observe(viewLifecycleOwner) {
|
playbackModel.song.observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
Log.d(this::class.simpleName, "Updating song display to ${it.name}")
|
Log.d(this::class.simpleName, "Updating song display to ${it.name}")
|
||||||
|
|
||||||
|
@ -78,7 +80,7 @@ class CompactPlaybackFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.formattedSeekBarProgress.observe(viewLifecycleOwner) {
|
playbackModel.positionAsProgress.observe(viewLifecycleOwner) {
|
||||||
binding.playbackProgress.progress = it
|
binding.playbackProgress.progress = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
binding.playbackModel = playbackModel
|
binding.playbackModel = playbackModel
|
||||||
binding.song = playbackModel.currentSong.value!!
|
binding.song = playbackModel.song.value!!
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener {
|
||||||
|
@ -73,14 +73,14 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.currentSong.observe(viewLifecycleOwner) {
|
playbackModel.song.observe(viewLifecycleOwner) {
|
||||||
Log.d(this::class.simpleName, "Updating song display to ${it.name}.")
|
Log.d(this::class.simpleName, "Updating song display to ${it.name}.")
|
||||||
|
|
||||||
binding.song = it
|
binding.song = it
|
||||||
binding.playbackSeekBar.max = it.seconds.toInt()
|
binding.playbackSeekBar.max = it.seconds.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.currentIndex.observe(viewLifecycleOwner) {
|
playbackModel.index.observe(viewLifecycleOwner) {
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
binding.playbackSkipPrev.enable(requireContext())
|
binding.playbackSkipPrev.enable(requireContext())
|
||||||
} else {
|
} else {
|
||||||
|
@ -129,11 +129,11 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates for the current duration TextView/Seekbar
|
// Updates for the current duration TextView/Seekbar
|
||||||
playbackModel.formattedCurrentDuration.observe(viewLifecycleOwner) {
|
playbackModel.formattedPosition.observe(viewLifecycleOwner) {
|
||||||
binding.playbackDurationCurrent.text = it
|
binding.playbackDurationCurrent.text = it
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.formattedSeekBarProgress.observe(viewLifecycleOwner) {
|
playbackModel.positionAsProgress.observe(viewLifecycleOwner) {
|
||||||
binding.playbackSeekBar.progress = it
|
binding.playbackSeekBar.progress = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
// Seeking callbacks
|
// Seeking callbacks
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
playbackModel.updateCurrentDurationWithProgress(progress)
|
playbackModel.updatePositionWithProgress(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,10 @@ import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toURI
|
import org.oxycblt.auxio.music.toURI
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
class PlaybackService : Service(), Player.EventListener {
|
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
private val player: SimpleExoPlayer by lazy {
|
private val player: SimpleExoPlayer by lazy {
|
||||||
val p = SimpleExoPlayer.Builder(applicationContext).build()
|
val p = SimpleExoPlayer.Builder(applicationContext).build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
@ -22,24 +24,33 @@ class PlaybackService : Service(), Player.EventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mBinder = LocalBinder()
|
private val mBinder = LocalBinder()
|
||||||
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
return mBinder
|
return mBinder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
playbackManager.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
player.release()
|
player.release()
|
||||||
|
playbackManager.removeCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playSong(song: Song) {
|
override fun onSongUpdate(song: Song?) {
|
||||||
val item = MediaItem.fromUri(song.id.toURI())
|
song?.let {
|
||||||
|
val item = MediaItem.fromUri(it.id.toURI())
|
||||||
player.setMediaItem(item)
|
player.setMediaItem(item)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
fun getService() = this@PlaybackService
|
fun getService() = this@PlaybackService
|
||||||
|
|
|
@ -11,55 +11,60 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.exoplayer2.Player
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toDuration
|
import org.oxycblt.auxio.music.toDuration
|
||||||
import kotlin.random.Random
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import kotlin.random.Random.Default.nextLong
|
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
// TODO: User managed queue
|
// The UI frontend for PlaybackStateManager.
|
||||||
// TODO: Add the playback service itself
|
class PlaybackViewModel(private val context: Context) : ViewModel(), PlaybackStateCallback {
|
||||||
// TODO: Add loop control [From playback]
|
// Playback
|
||||||
// TODO: Implement persistence through Bundles [I want to keep my shuffles, okay?]
|
private val mSong = MutableLiveData<Song>()
|
||||||
// A ViewModel that acts as an intermediary between PlaybackService and the Playback Fragments.
|
val song: LiveData<Song> get() = mSong
|
||||||
class PlaybackViewModel(private val context: Context) : ViewModel(), Player.EventListener {
|
|
||||||
private val mCurrentSong = MutableLiveData<Song>()
|
|
||||||
val currentSong: LiveData<Song> get() = mCurrentSong
|
|
||||||
|
|
||||||
private val mCurrentParent = MutableLiveData<BaseModel>()
|
private val mPosition = MutableLiveData(0L)
|
||||||
val currentParent: LiveData<BaseModel> get() = mCurrentParent
|
val position: LiveData<Long> get() = mPosition
|
||||||
|
|
||||||
|
// Queue
|
||||||
private val mQueue = MutableLiveData(mutableListOf<Song>())
|
private val mQueue = MutableLiveData(mutableListOf<Song>())
|
||||||
val queue: LiveData<MutableList<Song>> get() = mQueue
|
val queue: LiveData<MutableList<Song>> get() = mQueue
|
||||||
|
|
||||||
private val mCurrentIndex = MutableLiveData(0)
|
private val mIndex = MutableLiveData(0)
|
||||||
val currentIndex: LiveData<Int> get() = mCurrentIndex
|
val index: LiveData<Int> get() = mIndex
|
||||||
|
|
||||||
private val mCurrentMode = MutableLiveData(PlaybackMode.ALL_SONGS)
|
|
||||||
val currentMode: LiveData<PlaybackMode> get() = mCurrentMode
|
|
||||||
|
|
||||||
private val mCurrentDuration = MutableLiveData(0L)
|
|
||||||
|
|
||||||
|
// States
|
||||||
private val mIsPlaying = MutableLiveData(false)
|
private val mIsPlaying = MutableLiveData(false)
|
||||||
val isPlaying: LiveData<Boolean> get() = mIsPlaying
|
val isPlaying: LiveData<Boolean> get() = mIsPlaying
|
||||||
|
|
||||||
private val mIsShuffling = MutableLiveData(false)
|
private val mIsShuffling = MutableLiveData(false)
|
||||||
val isShuffling: LiveData<Boolean> get() = mIsShuffling
|
val isShuffling: LiveData<Boolean> get() = mIsShuffling
|
||||||
|
|
||||||
private val mShuffleSeed = MutableLiveData(-1L)
|
// Other
|
||||||
val shuffleSeed: LiveData<Long> get() = mShuffleSeed
|
|
||||||
|
|
||||||
private val mIsSeeking = MutableLiveData(false)
|
private val mIsSeeking = MutableLiveData(false)
|
||||||
val isSeeking: LiveData<Boolean> get() = mIsSeeking
|
val isSeeking: LiveData<Boolean> get() = mIsSeeking
|
||||||
|
|
||||||
|
val formattedPosition = Transformations.map(mPosition) {
|
||||||
|
it.toDuration()
|
||||||
|
}
|
||||||
|
|
||||||
|
val positionAsProgress = Transformations.map(mPosition) {
|
||||||
|
if (mSong.value != null) it.toInt() else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextItemsInQueue = Transformations.map(mQueue) {
|
||||||
|
it.slice((mIndex.value!! + 1) until it.size)
|
||||||
|
}
|
||||||
|
|
||||||
private var mCanAnimate = false
|
private var mCanAnimate = false
|
||||||
val canAnimate: Boolean get() = mCanAnimate
|
val canAnimate: Boolean get() = mCanAnimate
|
||||||
|
|
||||||
|
// Service setup
|
||||||
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
private lateinit var playbackService: PlaybackService
|
private lateinit var playbackService: PlaybackService
|
||||||
private var playbackIntent: Intent
|
private var playbackIntent: Intent
|
||||||
|
|
||||||
|
@ -79,342 +84,137 @@ class PlaybackViewModel(private val context: Context) : ViewModel(), Player.Even
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatted variants of the duration
|
init {
|
||||||
val formattedCurrentDuration = Transformations.map(mCurrentDuration) {
|
playbackManager.addCallback(this)
|
||||||
it.toDuration()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val formattedSeekBarProgress = Transformations.map(mCurrentDuration) {
|
// --- PLAYING FUNCTIONS ---
|
||||||
if (mCurrentSong.value != null) it.toInt() else 0
|
|
||||||
|
fun playSong(song: Song, mode: PlaybackMode) {
|
||||||
|
playbackManager.playSong(song, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatted queue that shows all the songs after the current playing song.
|
fun shuffleAll() {
|
||||||
val formattedQueue = Transformations.map(mQueue) {
|
playbackManager.shuffleAll()
|
||||||
it.slice((mCurrentIndex.value!! + 1) until it.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the current song while changing the queue mode.
|
fun playAlbum(album: Album, shuffled: Boolean) {
|
||||||
fun update(song: Song, mode: PlaybackMode) {
|
|
||||||
// Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible
|
|
||||||
// to determine what genre a song has.
|
|
||||||
if (mode == PlaybackMode.IN_GENRE) {
|
|
||||||
Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Updating song to ${song.name} and mode to $mode")
|
|
||||||
|
|
||||||
val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
mCurrentMode.value = mode
|
|
||||||
|
|
||||||
updatePlayback(song)
|
|
||||||
|
|
||||||
mQueue.value = when (mode) {
|
|
||||||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
|
||||||
PlaybackMode.IN_ARTIST -> song.album.artist.songs
|
|
||||||
PlaybackMode.IN_ALBUM -> song.album.songs
|
|
||||||
PlaybackMode.IN_GENRE -> error("what")
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrentParent.value = when (mode) {
|
|
||||||
PlaybackMode.ALL_SONGS -> null
|
|
||||||
PlaybackMode.IN_ARTIST -> song.album.artist
|
|
||||||
PlaybackMode.IN_ALBUM -> song.album
|
|
||||||
PlaybackMode.IN_GENRE -> error("what")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mIsShuffling.value!!) {
|
|
||||||
genShuffle(true)
|
|
||||||
} else {
|
|
||||||
resetShuffle()
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrentIndex.value = mQueue.value!!.indexOf(song)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play some parent music model, whether that being albums/artists/genres.
|
|
||||||
fun play(album: Album, isShuffled: Boolean) {
|
|
||||||
Log.d(this::class.simpleName, "Playing album ${album.name}")
|
|
||||||
|
|
||||||
if (album.songs.isEmpty()) {
|
if (album.songs.isEmpty()) {
|
||||||
Log.e(this::class.simpleName, "Album is empty, not playing.")
|
Log.e(this::class.simpleName, "Album is empty, Not playing.")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val songs = orderSongsInAlbum(album)
|
playbackManager.playParentModel(album, shuffled)
|
||||||
|
|
||||||
updatePlayback(songs[0])
|
|
||||||
|
|
||||||
mQueue.value = songs
|
|
||||||
mCurrentIndex.value = 0
|
|
||||||
mCurrentParent.value = album
|
|
||||||
mIsShuffling.value = isShuffled
|
|
||||||
mCurrentMode.value = PlaybackMode.IN_ALBUM
|
|
||||||
|
|
||||||
if (mIsShuffling.value!!) {
|
|
||||||
genShuffle(false)
|
|
||||||
} else {
|
|
||||||
resetShuffle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(artist: Artist, isShuffled: Boolean) {
|
fun playArtist(artist: Artist, shuffled: Boolean) {
|
||||||
Log.d(this::class.simpleName, "Playing artist ${artist.name}")
|
|
||||||
|
|
||||||
if (artist.songs.isEmpty()) {
|
if (artist.songs.isEmpty()) {
|
||||||
Log.e(this::class.simpleName, "Artist is empty, not playing.")
|
Log.e(this::class.simpleName, "Artist is empty, Not playing.")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val songs = orderSongsInArtist(artist)
|
playbackManager.playParentModel(artist, shuffled)
|
||||||
|
|
||||||
updatePlayback(songs[0])
|
|
||||||
|
|
||||||
mQueue.value = songs
|
|
||||||
mCurrentIndex.value = 0
|
|
||||||
mCurrentParent.value = artist
|
|
||||||
mIsShuffling.value = isShuffled
|
|
||||||
mCurrentMode.value = PlaybackMode.IN_ARTIST
|
|
||||||
|
|
||||||
if (mIsShuffling.value!!) {
|
|
||||||
genShuffle(false)
|
|
||||||
} else {
|
|
||||||
resetShuffle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(genre: Genre, isShuffled: Boolean) {
|
fun playGenre(genre: Genre, shuffled: Boolean) {
|
||||||
Log.d(this::class.simpleName, "Playing genre ${genre.name}")
|
|
||||||
|
|
||||||
if (genre.songs.isEmpty()) {
|
if (genre.songs.isEmpty()) {
|
||||||
Log.e(this::class.simpleName, "Genre is empty, not playing.")
|
Log.e(this::class.simpleName, "Genre is empty, Not playing.")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val songs = orderSongsInGenre(genre)
|
playbackManager.playParentModel(genre, shuffled)
|
||||||
|
|
||||||
updatePlayback(songs[0])
|
|
||||||
|
|
||||||
mQueue.value = songs
|
|
||||||
mCurrentIndex.value = 0
|
|
||||||
mCurrentParent.value = genre
|
|
||||||
mIsShuffling.value = isShuffled
|
|
||||||
mCurrentMode.value = PlaybackMode.IN_GENRE
|
|
||||||
|
|
||||||
if (mIsShuffling.value!!) {
|
|
||||||
genShuffle(false)
|
|
||||||
} else {
|
|
||||||
resetShuffle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the current duration using a SeekBar progress
|
// --- POSITION FUNCTIONS ---
|
||||||
fun updateCurrentDurationWithProgress(progress: Int) {
|
|
||||||
mCurrentDuration.value = progress.toLong()
|
fun updatePositionWithProgress(progress: Int) {
|
||||||
|
playbackManager.setPosition(progress.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invert, not directly set the playing/shuffling status
|
// --- QUEUE FUNCTIONS ---
|
||||||
// Used by the toggle buttons in playback fragment.
|
|
||||||
|
fun skipNext() {
|
||||||
|
playbackManager.skipNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipPrev() {
|
||||||
|
playbackManager.skipPrev()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeQueueItem(adapterIndex: Int) {
|
||||||
|
// Translate the adapter indices into the correct queue indices
|
||||||
|
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
|
||||||
|
|
||||||
|
val index = adapterIndex + delta
|
||||||
|
|
||||||
|
playbackManager.removeQueueItem(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveQueueItems(adapterFrom: Int, adapterTo: Int) {
|
||||||
|
// Translate the adapter indices into the correct queue indices
|
||||||
|
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
|
||||||
|
|
||||||
|
val from = adapterFrom + delta
|
||||||
|
val to = adapterTo + delta
|
||||||
|
|
||||||
|
playbackManager.moveQueueItems(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
||||||
fun invertPlayingStatus() {
|
fun invertPlayingStatus() {
|
||||||
mCanAnimate = true
|
mCanAnimate = true
|
||||||
|
|
||||||
mIsPlaying.value = !mIsPlaying.value!!
|
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invertShuffleStatus() {
|
fun invertShuffleStatus() {
|
||||||
mIsShuffling.value = !mIsShuffling.value!!
|
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
|
||||||
|
|
||||||
if (mIsShuffling.value!!) {
|
|
||||||
genShuffle(true)
|
|
||||||
} else {
|
|
||||||
resetShuffle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle all the songs.
|
// --- OTHER FUNCTIONS ---
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip to next song
|
|
||||||
fun skipNext() {
|
|
||||||
if (mCurrentIndex.value!! < mQueue.value!!.size) {
|
|
||||||
mCurrentIndex.value = mCurrentIndex.value!!.inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayback(mQueue.value!![mCurrentIndex.value!!])
|
|
||||||
|
|
||||||
forceQueueUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip to last song
|
|
||||||
fun skipPrev() {
|
|
||||||
if (mCurrentIndex.value!! > 0) {
|
|
||||||
mCurrentIndex.value = mCurrentIndex.value!!.dec()
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayback(mQueue.value!![mCurrentIndex.value!!])
|
|
||||||
|
|
||||||
forceQueueUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetAnimStatus() {
|
fun resetAnimStatus() {
|
||||||
mCanAnimate = false
|
mCanAnimate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move two queue items. Note that this function does not force-update the queue,
|
fun setSeekingStatus(value: Boolean) {
|
||||||
// as calling updateData with a drag would cause bugs.
|
mIsSeeking.value = value
|
||||||
fun moveQueueItems(adapterFrom: Int, adapterTo: Int) {
|
|
||||||
// Translate the adapter indices into the correct queue indices
|
|
||||||
val delta = mQueue.value!!.size - formattedQueue.value!!.size
|
|
||||||
|
|
||||||
val from = adapterFrom + delta
|
|
||||||
val to = adapterTo + delta
|
|
||||||
|
|
||||||
try {
|
|
||||||
val currentItem = mQueue.value!![from]
|
|
||||||
|
|
||||||
mQueue.value!!.removeAt(from)
|
|
||||||
mQueue.value!!.add(to, currentItem)
|
|
||||||
} catch (exception: IndexOutOfBoundsException) {
|
|
||||||
Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forceQueueUpdate()
|
// --- OVERRIDES ---
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a queue item. Note that this function does not force-update the queue,
|
|
||||||
// as calling updateData with a drag would cause bugs.
|
|
||||||
fun removeQueueItem(adapterIndex: Int) {
|
|
||||||
// Translate the adapter index into the correct queue index
|
|
||||||
val delta = mQueue.value!!.size - formattedQueue.value!!.size
|
|
||||||
val properIndex = adapterIndex + delta
|
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Removing item ${mQueue.value!![properIndex].name}.")
|
|
||||||
|
|
||||||
if (properIndex > mQueue.value!!.size || properIndex < 0) {
|
|
||||||
Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mQueue.value!!.removeAt(properIndex)
|
|
||||||
|
|
||||||
forceQueueUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force the observers of the queue to actually update after making changes.
|
|
||||||
private fun forceQueueUpdate() {
|
|
||||||
mQueue.value = mQueue.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic function for updating the playback with a new song.
|
|
||||||
// Use this instead of manually updating the values each time.
|
|
||||||
private fun updatePlayback(song: Song) {
|
|
||||||
mCurrentSong.value = song
|
|
||||||
mCurrentDuration.value = 0
|
|
||||||
|
|
||||||
if (!mIsPlaying.value!!) {
|
|
||||||
mIsPlaying.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackService.playSong(song)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force the observers to actually update.
|
|
||||||
mQueue.value = mQueue.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the queue and attempt to restore to the previous state
|
|
||||||
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.IN_GENRE -> orderSongsInGenre(mCurrentParent.value as Genre)
|
|
||||||
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 { album ->
|
|
||||||
final.addAll(album.songs.sortedBy { it.track })
|
|
||||||
}
|
|
||||||
|
|
||||||
return final
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
|
|
||||||
val final = mutableListOf<Song>()
|
|
||||||
|
|
||||||
genre.artists.sortedWith(
|
|
||||||
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
|
|
||||||
).forEach { artist ->
|
|
||||||
artist.albums.sortedByDescending { it.year }.forEach { album ->
|
|
||||||
final.addAll(album.songs.sortedBy { it.track })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return final
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
playbackManager.removeCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
context.unbindService(connection)
|
override fun onSongUpdate(song: Song?) {
|
||||||
|
song?.let {
|
||||||
|
mSong.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPositionUpdate(position: Long) {
|
||||||
|
mPosition.value = position
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueueUpdate(queue: MutableList<Song>) {
|
||||||
|
mQueue.value = queue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIndexUpdate(index: Int) {
|
||||||
|
mIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
|
mIsPlaying.value = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShuffleUpdate(isShuffling: Boolean) {
|
||||||
|
mIsShuffling.value = isShuffling
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
|
|
|
@ -16,7 +16,9 @@ import org.oxycblt.auxio.theme.applyDivider
|
||||||
import org.oxycblt.auxio.theme.toColor
|
import org.oxycblt.auxio.theme.toColor
|
||||||
|
|
||||||
class QueueFragment : BottomSheetDialogFragment() {
|
class QueueFragment : BottomSheetDialogFragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels {
|
||||||
|
PlaybackViewModel.Factory(requireActivity().application)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getTheme(): Int = R.style.Theme_BottomSheetFix
|
override fun getTheme(): Int = R.style.Theme_BottomSheetFix
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
playbackModel.formattedQueue.observe(viewLifecycleOwner) {
|
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
|
||||||
// If the first item is being moved, then scroll to the top position on completion
|
// If the first item is being moved, then scroll to the top position on completion
|
||||||
// to prevent ListAdapter from scrolling uncontrollably.
|
// to prevent ListAdapter from scrolling uncontrollably.
|
||||||
if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) {
|
if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
// Enum for instruction how the queue should function.
|
// Enum for instruction how the queue should function.
|
||||||
// ALL SONGS -> Play from all songs
|
// ALL SONGS -> Play from all songs
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
|
interface PlaybackStateCallback {
|
||||||
|
fun onSongUpdate(song: Song?) {}
|
||||||
|
fun onPositionUpdate(position: Long) {}
|
||||||
|
fun onQueueUpdate(queue: MutableList<Song>) {}
|
||||||
|
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||||
|
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||||
|
fun onIndexUpdate(index: Int) {}
|
||||||
|
}
|
|
@ -0,0 +1,344 @@
|
||||||
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
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.Header
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
||||||
|
// Never use this for ANYTHING UI related, that's what PlaybackViewModel is for.
|
||||||
|
class PlaybackStateManager {
|
||||||
|
// Playback
|
||||||
|
private var mSong: Song? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onSongUpdate(value) }
|
||||||
|
}
|
||||||
|
private var mPosition: Long = 0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onPositionUpdate(value) }
|
||||||
|
}
|
||||||
|
private var mParent: BaseModel? = null
|
||||||
|
|
||||||
|
// Queue
|
||||||
|
private var mQueue = mutableListOf<Song>()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onQueueUpdate(value) }
|
||||||
|
}
|
||||||
|
private var mIndex = 0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onIndexUpdate(value) }
|
||||||
|
}
|
||||||
|
private var mMode = PlaybackMode.ALL_SONGS
|
||||||
|
|
||||||
|
private var mIsPlaying = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onPlayingUpdate(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mIsShuffling = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
callbacks.forEach { it.onShuffleUpdate(value) }
|
||||||
|
}
|
||||||
|
private var mShuffleSeed = -1L
|
||||||
|
|
||||||
|
val isPlaying: Boolean get() = mIsPlaying
|
||||||
|
val isShuffling: Boolean get() = mIsShuffling
|
||||||
|
|
||||||
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
|
private val callbacks = mutableListOf<PlaybackStateCallback>()
|
||||||
|
|
||||||
|
fun addCallback(callback: PlaybackStateCallback) {
|
||||||
|
callbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCallback(callback: PlaybackStateCallback) {
|
||||||
|
callbacks.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
|
fun playSong(song: Song, mode: PlaybackMode) {
|
||||||
|
// Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible
|
||||||
|
// to determine what genre a song has.
|
||||||
|
if (mode == PlaybackMode.IN_GENRE) {
|
||||||
|
Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(this::class.simpleName, "Updating song to ${song.name} and mode to $mode")
|
||||||
|
|
||||||
|
val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
|
mMode = mode
|
||||||
|
|
||||||
|
updatePlayback(song)
|
||||||
|
|
||||||
|
mQueue = when (mode) {
|
||||||
|
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||||
|
PlaybackMode.IN_ARTIST -> song.album.artist.songs
|
||||||
|
PlaybackMode.IN_ALBUM -> song.album.songs
|
||||||
|
PlaybackMode.IN_GENRE -> error("what")
|
||||||
|
}
|
||||||
|
|
||||||
|
mParent = when (mode) {
|
||||||
|
PlaybackMode.ALL_SONGS -> null
|
||||||
|
PlaybackMode.IN_ARTIST -> song.album.artist
|
||||||
|
PlaybackMode.IN_ALBUM -> song.album
|
||||||
|
PlaybackMode.IN_GENRE -> error("what")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mIsShuffling) {
|
||||||
|
genShuffle(true)
|
||||||
|
} else {
|
||||||
|
resetShuffle()
|
||||||
|
}
|
||||||
|
|
||||||
|
mIndex = mQueue.indexOf(song)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playParentModel(baseModel: BaseModel, shuffled: Boolean) {
|
||||||
|
if (baseModel is Song || baseModel is Header) {
|
||||||
|
Log.e(
|
||||||
|
this::class.simpleName,
|
||||||
|
"playParentModel does not support ${baseModel::class.simpleName}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(this::class.simpleName, "Playing ${baseModel.name}")
|
||||||
|
|
||||||
|
when (baseModel) {
|
||||||
|
is Album -> {
|
||||||
|
mQueue = orderSongsInAlbum(baseModel)
|
||||||
|
mMode = PlaybackMode.IN_ALBUM
|
||||||
|
}
|
||||||
|
is Artist -> {
|
||||||
|
mQueue = orderSongsInArtist(baseModel)
|
||||||
|
mMode = PlaybackMode.IN_ARTIST
|
||||||
|
}
|
||||||
|
is Genre -> {
|
||||||
|
mQueue = orderSongsInGenre(baseModel)
|
||||||
|
mMode = PlaybackMode.IN_GENRE
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> error("what")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayback(mQueue[0])
|
||||||
|
|
||||||
|
mIndex = 0
|
||||||
|
mParent = baseModel
|
||||||
|
mIsShuffling = shuffled
|
||||||
|
|
||||||
|
if (mIsShuffling) {
|
||||||
|
genShuffle(false)
|
||||||
|
} else {
|
||||||
|
resetShuffle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlayback(song: Song) {
|
||||||
|
mSong = song
|
||||||
|
mPosition = 0
|
||||||
|
|
||||||
|
if (!mIsPlaying) {
|
||||||
|
mIsPlaying = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPosition(position: Long) {
|
||||||
|
mPosition = position
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
|
fun skipNext() {
|
||||||
|
if (mIndex < mQueue.size) {
|
||||||
|
mIndex = mIndex.inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayback(mQueue[mIndex])
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipPrev() {
|
||||||
|
if (mIndex > 0) {
|
||||||
|
mIndex = mIndex.dec()
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayback(mQueue[mIndex])
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeQueueItem(index: Int) {
|
||||||
|
Log.d(this::class.simpleName, "Removing item ${mQueue[index].name}.")
|
||||||
|
|
||||||
|
if (index > mQueue.size || index < 0) {
|
||||||
|
Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mQueue.removeAt(index)
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveQueueItems(from: Int, to: Int) {
|
||||||
|
try {
|
||||||
|
val currentItem = mQueue[from]
|
||||||
|
|
||||||
|
mQueue.removeAt(from)
|
||||||
|
mQueue.add(to, currentItem)
|
||||||
|
} catch (exception: IndexOutOfBoundsException) {
|
||||||
|
Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceQueueUpdate() {
|
||||||
|
mQueue = mQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SHUFFLE FUNCTIONS ---
|
||||||
|
|
||||||
|
fun shuffleAll() {
|
||||||
|
val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
|
mIsShuffling = true
|
||||||
|
mQueue = musicStore.songs.toMutableList()
|
||||||
|
mMode = PlaybackMode.ALL_SONGS
|
||||||
|
mIndex = 0
|
||||||
|
|
||||||
|
genShuffle(false)
|
||||||
|
updatePlayback(mQueue[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = newSeed
|
||||||
|
|
||||||
|
mQueue.shuffle(Random(newSeed))
|
||||||
|
mIndex = 0
|
||||||
|
|
||||||
|
// If specified, make the current song the first member of the queue.
|
||||||
|
if (keepSong) {
|
||||||
|
mSong?.let {
|
||||||
|
mQueue.remove(it)
|
||||||
|
mQueue.add(0, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, just start from the zeroth position in the queue.
|
||||||
|
mSong = mQueue[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the queue and attempt to restore to the previous state
|
||||||
|
private fun resetShuffle() {
|
||||||
|
mShuffleSeed = -1
|
||||||
|
|
||||||
|
mQueue = when (mMode) {
|
||||||
|
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||||
|
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||||
|
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||||
|
PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
mIndex = mQueue.indexOf(mSong)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATE FUNCTIONS ---
|
||||||
|
|
||||||
|
fun setPlayingStatus(value: Boolean) {
|
||||||
|
if (mIsPlaying != value) {
|
||||||
|
mIsPlaying = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShuffleStatus(value: Boolean) {
|
||||||
|
mIsShuffling = value
|
||||||
|
|
||||||
|
if (mIsShuffling) {
|
||||||
|
genShuffle(true)
|
||||||
|
} else {
|
||||||
|
resetShuffle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ORDERING FUNCTIONS ---
|
||||||
|
|
||||||
|
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 { album ->
|
||||||
|
final.addAll(album.songs.sortedBy { it.track })
|
||||||
|
}
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
|
||||||
|
val final = mutableListOf<Song>()
|
||||||
|
|
||||||
|
genre.artists.sortedWith(
|
||||||
|
compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||||
|
).forEach { artist ->
|
||||||
|
artist.albums.sortedByDescending { it.year }.forEach { album ->
|
||||||
|
final.addAll(album.songs.sortedBy { it.track })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: PlaybackStateManager? = null
|
||||||
|
|
||||||
|
fun getInstance(): PlaybackStateManager {
|
||||||
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
if (currentInstance != null) {
|
||||||
|
return currentInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
val newInstance = PlaybackStateManager()
|
||||||
|
INSTANCE = newInstance
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SongsFragment : Fragment() {
|
||||||
|
|
||||||
binding.songRecycler.apply {
|
binding.songRecycler.apply {
|
||||||
adapter = SongAdapter(musicStore.songs) {
|
adapter = SongAdapter(musicStore.songs) {
|
||||||
playbackModel.update(it, PlaybackMode.ALL_SONGS)
|
playbackModel.playSong(it, PlaybackMode.ALL_SONGS)
|
||||||
}
|
}
|
||||||
applyDivider()
|
applyDivider()
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
|
|
Loading…
Reference in a new issue