playback: add ability to play/shuffle selection

Add the ability to play or shuffle a selection.

This finally allows "arbitrary" playback to be created from any
combination of songs/albums/artist/genres, rather than just from
pre-defined options.

Resolves #313.
This commit is contained in:
Alexander Capehart 2023-01-10 08:12:47 -07:00
parent 692839e8fe
commit 5988908b56
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 50 additions and 42 deletions

View file

@ -71,6 +71,14 @@ abstract class SelectionFragment<VB : ViewBinding> :
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true true
} }
R.id.action_selection_play -> {
playbackModel.play(selectionModel.consume())
true
}
R.id.action_selection_shuffle -> {
playbackModel.shuffle(selectionModel.consume())
true
}
else -> false else -> false
} }
} }

View file

@ -58,7 +58,7 @@ class MusicStore private constructor() {
} }
/** /**
* Remove a [Listener] from this instance, preventing it from recieving any further updates. * Remove a [Listener] from this instance, preventing it from receiving any further updates.
* @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in * @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in
* the first place. * the first place.
* @see Listener * @see Listener

View file

@ -129,7 +129,6 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean) data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
// TODO: Unify include + exclude
/** /**
* A mime type of a file. Only intended for display. * A mime type of a file. Only intended for display.

View file

@ -196,7 +196,7 @@ val StorageVolume.isInternalCompat: Boolean
get() = isPrimaryCompat && isEmulatedCompat get() = isPrimaryCompat && isEmulatedCompat
/** /**
* The unique identifier for this [StorageVolume], obtained in a version compatible manner Can be * The unique identifier for this [StorageVolume], obtained in a version compatible manner. Can be
* null. * null.
* @see StorageVolume.getUuid * @see StorageVolume.getUuid
*/ */

View file

@ -38,6 +38,7 @@ class PlaybackViewModel(application: Application) :
private val musicSettings = MusicSettings.from(application) private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application) private val playbackSettings = PlaybackSettings.from(application)
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val musicStore = MusicStore.getInstance()
private var lastPositionJob: Job? = null private var lastPositionJob: Job? = null
private val _song = MutableStateFlow<Song?>(null) private val _song = MutableStateFlow<Song?>(null)
@ -161,27 +162,13 @@ class PlaybackViewModel(application: Application) :
*/ */
fun playFrom(song: Song, playbackMode: MusicMode) { fun playFrom(song: Song, playbackMode: MusicMode) {
when (playbackMode) { when (playbackMode) {
MusicMode.SONGS -> playFromAll(song) MusicMode.SONGS -> playImpl(song, null)
MusicMode.ALBUMS -> playFromAlbum(song) MusicMode.ALBUMS -> playImpl(song, song.album)
MusicMode.ARTISTS -> playFromArtist(song) MusicMode.ARTISTS -> playFromArtist(song)
MusicMode.GENRES -> playFromGenre(song) MusicMode.GENRES -> playFromGenre(song)
} }
} }
/**
* Play the given [Song] from all songs in the music library.
* @param song The [Song] to play.
*/
fun playFromAll(song: Song) {
playImpl(song, null)
}
/**
* Play a [Song] from it's [Album].
* @param song The [Song] to play.
*/
fun playFromAlbum(song: Song) = playImpl(song, song.album)
/** /**
* Play a [Song] from one of it's [Artist]s. * Play a [Song] from one of it's [Artist]s.
* @param song The [Song] to play. * @param song The [Song] to play.
@ -250,6 +237,13 @@ class PlaybackViewModel(application: Application) :
*/ */
fun play(genre: Genre) = playImpl(null, genre, false) fun play(genre: Genre) = playImpl(null, genre, false)
/**
* Play a [Music] selection.
* @param selection The selection to play.
*/
fun play(selection: List<Music>) =
playbackManager.play(null, selectionToSongs(selection), false)
/** /**
* Shuffle an [Album]. * Shuffle an [Album].
* @param album The [Album] to shuffle. * @param album The [Album] to shuffle.
@ -269,13 +263,11 @@ class PlaybackViewModel(application: Application) :
fun shuffle(genre: Genre) = playImpl(null, genre, true) fun shuffle(genre: Genre) = playImpl(null, genre, true)
/** /**
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to * Shuffle a [Music] selection.
* enqueue a playback action at startup to then occur when the music library is fully loaded. * @param selection The selection to shuffle.
* @param action The [InternalPlayer.Action] to perform eventually.
*/ */
fun startAction(action: InternalPlayer.Action) { fun shuffle(selection: List<Music>) =
playbackManager.startAction(action) playbackManager.play(null, selectionToSongs(selection), true)
}
private fun playImpl( private fun playImpl(
song: Song?, song: Song?,
@ -285,6 +277,7 @@ class PlaybackViewModel(application: Application) :
check(song == null || parent == null || parent.songs.contains(song)) { check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent" "Song to play not in parent"
} }
val library = musicStore.library ?: return
val sort = val sort =
when (parent) { when (parent) {
is Genre -> musicSettings.genreSongSort is Genre -> musicSettings.genreSongSort
@ -292,7 +285,17 @@ class PlaybackViewModel(application: Application) :
is Album -> musicSettings.albumSongSort is Album -> musicSettings.albumSongSort
null -> musicSettings.songSort null -> musicSettings.songSort
} }
playbackManager.play(song, parent, sort, shuffled) val queue = sort.songs(parent?.songs ?: library.songs)
playbackManager.play(song, queue, shuffled)
}
/**
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to
* enqueue a playback action at startup to then occur when the music library is fully loaded.
* @param action The [InternalPlayer.Action] to perform eventually.
*/
fun startAction(action: InternalPlayer.Action) {
playbackManager.startAction(action)
} }
// --- PLAYER FUNCTIONS --- // --- PLAYER FUNCTIONS ---

View file

@ -155,20 +155,21 @@ class PlaybackStateManager private constructor() {
/** /**
* Start new playback. * Start new playback.
* @param song A particular [Song] to play, or null to play the first [Song] in the new queue. * @param song A particular [Song] to play, or null to play the first [Song] in the new queue.
* @param queue The queue of [Song]s to play from.
* @param parent The [MusicParent] to play from, or null if to play from the entire [Library]. * @param parent The [MusicParent] to play from, or null if to play from the entire [Library].
* @param sort [Sort] to initially sort an ordered queue with. * @param sort [Sort] to initially sort an ordered queue with.
* @param shuffled Whether to shuffle or not. * @param shuffled Whether to shuffle or not.
*/ */
@Synchronized @Synchronized
fun play(song: Song?, parent: MusicParent?, sort: Sort, shuffled: Boolean) { fun play(song: Song?, queue: List<Song>, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return val library = musicStore.library ?: return
// Set up parent and queue // Set up parent and queue
this.parent = parent this.parent = parent
queue.start(song, sort.songs(parent?.songs ?: library.songs), shuffled) this.queue.start(song, queue, shuffled)
// Notify components of changes // Notify components of changes
notifyNewPlayback() notifyNewPlayback()
internalPlayer.loadSong(queue.currentSong, true) internalPlayer.loadSong(this.queue.currentSong, true)
// Played something, so we are initialized now // Played something, so we are initialized now
isInitialized = true isInitialized = true
} }

View file

@ -355,15 +355,14 @@ class PlaybackService :
} }
// Shuffle all -> Start new playback from all songs // Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> { is InternalPlayer.Action.ShuffleAll -> {
playbackManager.play(null, null, musicSettings.songSort, true) playbackManager.play(null, musicSettings.songSort.songs(library.songs), true)
} }
// Open -> Try to find the Song for the given file and then play it from all songs // Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> { is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song -> library.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play( playbackManager.play(
song, song,
null, musicSettings.songSort.songs(library.songs),
musicSettings.songSort,
playbackManager.queue.isShuffled && playbackSettings.keepShuffle) playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
} }
} }

View file

@ -8,16 +8,14 @@
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_selection_queue_add" android:id="@+id/action_selection_queue_add"
android:icon="@drawable/ic_play_next"
android:title="@string/lbl_queue_add" android:title="@string/lbl_queue_add"
app:showAsAction="never" /> app:showAsAction="never" />
<!-- TOOD: Disabled until able to get queue system into shape --> <item
<!-- <item--> android:id="@+id/action_selection_play"
<!-- android:id="@+id/action_play_selection"--> android:title="@string/lbl_play_selected"
<!-- android:title="@string/lbl_play_selected"--> app:showAsAction="never"/>
<!-- app:showAsAction="never"/>--> <item
<!-- <item--> android:id="@+id/action_selection_shuffle"
<!-- android:id="@+id/action_shuffle_selection"--> android:title="@string/lbl_shuffle_selected"
<!-- android:title="@string/lbl_shuffle_selected"--> app:showAsAction="never"/>
<!-- app:showAsAction="never"/>-->
</menu> </menu>