diff --git a/CHANGELOG.md b/CHANGELOG.md index b1bd13e9b..24adc7035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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" diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 8b0c32112..67c80091c 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt b/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt new file mode 100644 index 000000000..1c741aa56 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt @@ -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 +} \ No newline at end of file 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 c46f6ad94..143253b99 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -242,7 +242,7 @@ class PlaybackViewModel(application: Application) : * @param selection The selection to play. */ fun play(selection: List) = - 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) = - 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) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 396e1eb92..7929dd4ed 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -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(), 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(), EditableListL min(queue.lastIndex, scrollTo + (end - start))) } } - queueModel.finishScrollTo() - // Update position in list (and thus past/future items) - queueAdapter.setPosition(index, isPlaying) + queueModel.finishInstructions() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 5c5e5d8d3..1c4709e15 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -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 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?) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 2cc2cf99d..8b729379c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -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, shuffled: Boolean) { + fun play(song: Song?, parent: MusicParent?, queue: List, 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) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt index 7759f0b3d..6638901fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index aa2d254d0..08b820a5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index bb4d9109b..bd6dfe9f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -123,15 +123,13 @@ class AboutFragment : ViewBindingFragment() { 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 diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index c1804d859..92a81fa26 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -88,6 +88,11 @@ interface Settings { 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) {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index fdc0c9671..1a7a9ec89 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -56,7 +56,7 @@ abstract class BaseBottomSheetBehavior(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 diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index fd362131d..ae53a8a7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -74,12 +74,11 @@ abstract class ViewBindingDialogFragment : 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, diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index b5ece20e2..aaaf3119e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -65,12 +65,11 @@ abstract class ViewBindingFragment : 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,