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)
true
}
R.id.action_selection_play -> {
playbackModel.play(selectionModel.consume())
true
}
R.id.action_selection_shuffle -> {
playbackModel.shuffle(selectionModel.consume())
true
}
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
* the first place.
* @see Listener

View file

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

View file

@ -196,7 +196,7 @@ val StorageVolume.isInternalCompat: Boolean
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.
* @see StorageVolume.getUuid
*/

View file

@ -38,6 +38,7 @@ class PlaybackViewModel(application: Application) :
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val playbackManager = PlaybackStateManager.getInstance()
private val musicStore = MusicStore.getInstance()
private var lastPositionJob: Job? = null
private val _song = MutableStateFlow<Song?>(null)
@ -161,27 +162,13 @@ class PlaybackViewModel(application: Application) :
*/
fun playFrom(song: Song, playbackMode: MusicMode) {
when (playbackMode) {
MusicMode.SONGS -> playFromAll(song)
MusicMode.ALBUMS -> playFromAlbum(song)
MusicMode.SONGS -> playImpl(song, null)
MusicMode.ALBUMS -> playImpl(song, song.album)
MusicMode.ARTISTS -> playFromArtist(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.
* @param song The [Song] to play.
@ -250,6 +237,13 @@ class PlaybackViewModel(application: Application) :
*/
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].
* @param album The [Album] to shuffle.
@ -269,13 +263,11 @@ class PlaybackViewModel(application: Application) :
fun shuffle(genre: Genre) = playImpl(null, genre, true)
/**
* 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.
* Shuffle a [Music] selection.
* @param selection The selection to shuffle.
*/
fun startAction(action: InternalPlayer.Action) {
playbackManager.startAction(action)
}
fun shuffle(selection: List<Music>) =
playbackManager.play(null, selectionToSongs(selection), true)
private fun playImpl(
song: Song?,
@ -285,6 +277,7 @@ class PlaybackViewModel(application: Application) :
check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent"
}
val library = musicStore.library ?: return
val sort =
when (parent) {
is Genre -> musicSettings.genreSongSort
@ -292,7 +285,17 @@ class PlaybackViewModel(application: Application) :
is Album -> musicSettings.albumSongSort
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 ---

View file

@ -155,20 +155,21 @@ class PlaybackStateManager private constructor() {
/**
* Start new playback.
* @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 sort [Sort] to initially sort an ordered queue with.
* @param shuffled Whether to shuffle or not.
*/
@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 library = musicStore.library ?: return
// Set up parent and queue
this.parent = parent
queue.start(song, sort.songs(parent?.songs ?: library.songs), shuffled)
this.queue.start(song, queue, shuffled)
// Notify components of changes
notifyNewPlayback()
internalPlayer.loadSong(queue.currentSong, true)
internalPlayer.loadSong(this.queue.currentSong, true)
// Played something, so we are initialized now
isInitialized = true
}

View file

@ -355,15 +355,14 @@ class PlaybackService :
}
// Shuffle all -> Start new playback from all songs
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
is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(
song,
null,
musicSettings.songSort,
musicSettings.songSort.songs(library.songs),
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
}
}

View file

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