list: add update instructions framework
Add the basic framework that should allow for different types of list updates in different situations.
This commit is contained in:
parent
5988908b56
commit
176f0cc465
14 changed files with 100 additions and 88 deletions
|
@ -2,6 +2,9 @@
|
|||
|
||||
## dev
|
||||
|
||||
#### What's New
|
||||
- Added ability to play/shuffle selections
|
||||
|
||||
#### What's Improved
|
||||
- Added ability to edit previously played or currently playing items in the queue
|
||||
- Added support for date values formatted as "YYYYMMDD"
|
||||
|
|
|
@ -343,10 +343,8 @@ class MainFragment :
|
|||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
// Queue sheet behavior is either collapsed or expanded, no hiding needed
|
||||
queueSheetBehavior?.isDraggable = true
|
||||
|
||||
playbackSheetBehavior.apply {
|
||||
// Make sure the view is draggable, at least until the draw checks kick in.
|
||||
isDraggable = true
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package org.oxycblt.auxio.list
|
||||
|
||||
/**
|
||||
* Represents the specific way to update a list of items.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
enum class UpdateInstructions {
|
||||
/**
|
||||
* (A)synchronously diff the list. This should be used for small diffs with little item
|
||||
* movement.
|
||||
*/
|
||||
DIFF,
|
||||
|
||||
/**
|
||||
* Synchronously remove the current list and replace it with a new one. This should be used
|
||||
* for large diffs with that would cause erratic scroll behavior or in-efficiency.
|
||||
*/
|
||||
REPLACE
|
||||
}
|
|
@ -242,7 +242,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param selection The selection to play.
|
||||
*/
|
||||
fun play(selection: List<Music>) =
|
||||
playbackManager.play(null, selectionToSongs(selection), false)
|
||||
playbackManager.play(null, null, selectionToSongs(selection), false)
|
||||
|
||||
/**
|
||||
* Shuffle an [Album].
|
||||
|
@ -267,7 +267,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param selection The selection to shuffle.
|
||||
*/
|
||||
fun shuffle(selection: List<Music>) =
|
||||
playbackManager.play(null, selectionToSongs(selection), true)
|
||||
playbackManager.play(null, null, selectionToSongs(selection), true)
|
||||
|
||||
private fun playImpl(
|
||||
song: Song?,
|
||||
|
@ -286,7 +286,7 @@ class PlaybackViewModel(application: Application) :
|
|||
null -> musicSettings.songSort
|
||||
}
|
||||
val queue = sort.songs(parent?.songs ?: library.songs)
|
||||
playbackManager.play(song, queue, shuffled)
|
||||
playbackManager.play(song, parent, queue, shuffled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.list.EditableListListener
|
||||
import org.oxycblt.auxio.list.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
|
@ -100,18 +101,19 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
|||
val binding = requireBinding()
|
||||
|
||||
// Replace or diff the queue depending on the type of change it is.
|
||||
// TODO: Extend this to the whole app.
|
||||
if (queueModel.replaceQueue == true) {
|
||||
val instructions = queueModel.instructions
|
||||
if (instructions?.update == UpdateInstructions.REPLACE) {
|
||||
logD("Replacing queue")
|
||||
queueAdapter.replaceList(queue)
|
||||
} else {
|
||||
logD("Diffing queue")
|
||||
queueAdapter.submitList(queue)
|
||||
}
|
||||
queueModel.finishReplace()
|
||||
// Update position in list (and thus past/future items)
|
||||
queueAdapter.setPosition(index, isPlaying)
|
||||
|
||||
// If requested, scroll to a new item (occurs when the index moves)
|
||||
val scrollTo = queueModel.scrollTo
|
||||
val scrollTo = instructions?.scrollTo
|
||||
if (scrollTo != null) {
|
||||
val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager
|
||||
val start = lmm.findFirstCompletelyVisibleItemPosition()
|
||||
|
@ -132,9 +134,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
|||
min(queue.lastIndex, scrollTo + (end - start)))
|
||||
}
|
||||
}
|
||||
queueModel.finishScrollTo()
|
||||
|
||||
// Update position in list (and thus past/future items)
|
||||
queueAdapter.setPosition(index, isPlaying)
|
||||
queueModel.finishInstructions()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback.queue
|
|||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.list.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
@ -42,15 +43,47 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
val index: StateFlow<Int>
|
||||
get() = _index
|
||||
|
||||
/** Whether to replace or diff the queue list when updating it. Is null if not specified. */
|
||||
var replaceQueue: Boolean? = null
|
||||
/** Flag to scroll to a particular queue item. Is null if no command has been specified. */
|
||||
var scrollTo: Int? = null
|
||||
/** Specifies how to update the list when the queue changes. */
|
||||
var instructions: Instructions? = null
|
||||
|
||||
init {
|
||||
playbackManager.addListener(this)
|
||||
}
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
instructions = Instructions(null, queue.index)
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
instructions = Instructions(UpdateInstructions.DIFF, null)
|
||||
_queue.value = queue.resolve()
|
||||
if (change != Queue.ChangeResult.MAPPING) {
|
||||
// Index changed, make sure it remains updated without actually scrolling to it.
|
||||
_index.value = queue.index
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueReordered(queue: Queue) {
|
||||
// Queue changed completely -> Replace queue, update index
|
||||
instructions = Instructions(UpdateInstructions.REPLACE, null)
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
// Entirely new queue -> Replace queue, update index
|
||||
instructions = Instructions(UpdateInstructions.REPLACE, null)
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
playbackManager.removeListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playing the the queue item at the given index.
|
||||
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
|
||||
|
@ -86,52 +119,10 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
return true
|
||||
}
|
||||
|
||||
/** Finish a replace flag specified by [replaceQueue]. */
|
||||
fun finishReplace() {
|
||||
replaceQueue = null
|
||||
/** Signal that the specified [Instructions] in [instructions] were performed. */
|
||||
fun finishInstructions() {
|
||||
instructions = null
|
||||
}
|
||||
|
||||
/** Finish a scroll operation started by [scrollTo]. */
|
||||
fun finishScrollTo() {
|
||||
scrollTo = null
|
||||
}
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
// Index moved -> Scroll to new index
|
||||
replaceQueue = null
|
||||
scrollTo = queue.index
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
replaceQueue = false
|
||||
scrollTo = null
|
||||
_queue.value = queue.resolve()
|
||||
if (change != Queue.ChangeResult.MAPPING) {
|
||||
// Index changed, make sure it remains updated without actually scrolling to it.
|
||||
_index.value = queue.index
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueReordered(queue: Queue) {
|
||||
// Queue changed completely -> Replace queue, update index
|
||||
replaceQueue = true
|
||||
scrollTo = queue.index
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
// Entirely new queue -> Replace queue, update index
|
||||
replaceQueue = true
|
||||
scrollTo = queue.index
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
playbackManager.removeListener(this)
|
||||
}
|
||||
class Instructions(val update: UpdateInstructions?, val scrollTo: Int?)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.library.Library
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
@ -60,7 +59,7 @@ class PlaybackStateManager private constructor() {
|
|||
val queue = Queue()
|
||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||
@Volatile
|
||||
var parent: MusicParent? = null
|
||||
var parent: MusicParent? = null // TODO: Parent is interpreted wrong when nothing is playing.
|
||||
private set
|
||||
|
||||
/** The current [InternalPlayer] state. */
|
||||
|
@ -98,7 +97,7 @@ class PlaybackStateManager 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
|
||||
|
@ -156,14 +155,13 @@ 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 parent The [MusicParent] to play from, or null if to play from an non-specific
|
||||
* collection of "All [Song]s".
|
||||
* @param shuffled Whether to shuffle or not.
|
||||
*/
|
||||
@Synchronized
|
||||
fun play(song: Song?, queue: List<Song>, shuffled: Boolean) {
|
||||
fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
val library = musicStore.library ?: return
|
||||
// Set up parent and queue
|
||||
this.parent = parent
|
||||
this.queue.start(song, queue, shuffled)
|
||||
|
|
|
@ -208,8 +208,9 @@ class Queue {
|
|||
// We have moved an song from in front of the playing song to behind, shift forward.
|
||||
in dst until src -> index += 1
|
||||
else -> {
|
||||
// Nothing to do.
|
||||
check()
|
||||
ChangeResult.MAPPING
|
||||
return ChangeResult.MAPPING
|
||||
}
|
||||
}
|
||||
check()
|
||||
|
|
|
@ -355,13 +355,14 @@ class PlaybackService :
|
|||
}
|
||||
// Shuffle all -> Start new playback from all songs
|
||||
is InternalPlayer.Action.ShuffleAll -> {
|
||||
playbackManager.play(null, musicSettings.songSort.songs(library.songs), true)
|
||||
playbackManager.play(null, 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.songs(library.songs),
|
||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
||||
}
|
||||
|
|
|
@ -123,15 +123,13 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
|||
if (pkgName == "android") {
|
||||
// No default browser [Must open app chooser, may not be supported]
|
||||
openAppChooser(browserIntent)
|
||||
} else {
|
||||
try {
|
||||
browserIntent.setPackage(pkgName)
|
||||
startActivity(browserIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Not a browser but an app chooser
|
||||
browserIntent.setPackage(null)
|
||||
openAppChooser(browserIntent)
|
||||
}
|
||||
} else try {
|
||||
browserIntent.setPackage(pkgName)
|
||||
startActivity(browserIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Not a browser but an app chooser
|
||||
browserIntent.setPackage(null)
|
||||
openAppChooser(browserIntent)
|
||||
}
|
||||
} else {
|
||||
// No app installed to open the link
|
||||
|
|
|
@ -88,6 +88,11 @@ interface Settings<L> {
|
|||
onSettingChanged(key, unlikelyToBeNull(listener))
|
||||
}
|
||||
|
||||
open fun onSettingChanged(key: String, listener: L) {}
|
||||
/**
|
||||
* Called when a setting entry with the given [key] has changed.
|
||||
* @param key The key of the changed setting.
|
||||
* @param listener The implementation's listener that updates should be applied to.
|
||||
*/
|
||||
protected open fun onSettingChanged(key: String, listener: L) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ abstract class BaseBottomSheetBehavior<V : View>(context: Context, attributeSet:
|
|||
/**
|
||||
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is
|
||||
* linked to.
|
||||
* @param child The child view recieving the [WindowInsets].
|
||||
* @param child The child view receiving the [WindowInsets].
|
||||
* @param insets The [WindowInsets] to apply.
|
||||
* @return The (possibly modified) [WindowInsets].
|
||||
* @see View.onApplyWindowInsets
|
||||
|
|
|
@ -74,12 +74,11 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
|
|||
* @return The currently-inflated [ViewBinding].
|
||||
* @throws IllegalStateException if the [ViewBinding] is not inflated.
|
||||
*/
|
||||
protected fun requireBinding(): VB {
|
||||
return requireNotNull(_binding) {
|
||||
protected fun requireBinding() =
|
||||
requireNotNull(_binding) {
|
||||
"ViewBinding was available. Fragment should be a valid state " +
|
||||
"right now, but instead it was ${lifecycle.currentState}"
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
|
@ -65,12 +65,11 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
|
|||
* @return The currently-inflated [ViewBinding].
|
||||
* @throws IllegalStateException if the [ViewBinding] is not inflated.
|
||||
*/
|
||||
protected fun requireBinding(): VB {
|
||||
return requireNotNull(_binding) {
|
||||
protected fun requireBinding() =
|
||||
requireNotNull(_binding) {
|
||||
"ViewBinding was available. Fragment should be a valid state " +
|
||||
"right now, but instead it was ${lifecycle.currentState}"
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
Loading…
Reference in a new issue