list: drop selections when navigating
Drop item selections when navigating to another view. This resolves issues that might occur if one were to navigate fast enough to another view after selecting something. If I were to use a unified toolbar, this wouldn't be needed, but that is a very far-flung addition.
This commit is contained in:
parent
0737dbace3
commit
7212700553
9 changed files with 60 additions and 30 deletions
|
@ -26,6 +26,8 @@ import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||||
|
@ -34,6 +36,7 @@ import com.google.android.material.transition.MaterialFadeThrough
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -51,11 +54,13 @@ import org.oxycblt.auxio.util.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class MainFragment :
|
class MainFragment :
|
||||||
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener {
|
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener, NavController.OnDestinationChangedListener {
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val callback = DynamicBackPressedCallback()
|
private val callback = DynamicBackPressedCallback()
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
|
private var initialNavDestinationChange = true
|
||||||
private val elevationNormal: Float by lifecycleObject { binding ->
|
private val elevationNormal: Float by lifecycleObject { binding ->
|
||||||
binding.context.getDimen(R.dimen.elevation_normal)
|
binding.context.getDimen(R.dimen.elevation_normal)
|
||||||
}
|
}
|
||||||
|
@ -128,14 +133,21 @@ class MainFragment :
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
val binding = requireBinding()
|
||||||
|
// Once we add the destination change callback, we will receive another initialization call,
|
||||||
|
// so handle that by resetting the flag.
|
||||||
|
initialNavDestinationChange = false
|
||||||
|
binding.exploreNavHost.findNavController().addOnDestinationChangedListener(this)
|
||||||
// Listener could still reasonably fire even if we clear the binding, attach/detach
|
// Listener could still reasonably fire even if we clear the binding, attach/detach
|
||||||
// our pre-draw listener our listener in onStart/onStop respectively.
|
// our pre-draw listener our listener in onStart/onStop respectively.
|
||||||
requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this)
|
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
requireBinding().playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
|
val binding = requireBinding()
|
||||||
|
binding.exploreNavHost.findNavController().removeOnDestinationChangedListener(this)
|
||||||
|
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreDraw(): Boolean {
|
override fun onPreDraw(): Boolean {
|
||||||
|
@ -228,6 +240,21 @@ class MainFragment :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestinationChanged(
|
||||||
|
controller: NavController,
|
||||||
|
destination: NavDestination,
|
||||||
|
arguments: Bundle?
|
||||||
|
) {
|
||||||
|
// Drop the initial call by NavController that simply provides us with the current
|
||||||
|
// destination. This would cause the selection state to be lost every time the device
|
||||||
|
// rotates.
|
||||||
|
if (!initialNavDestinationChange) {
|
||||||
|
initialNavDestinationChange = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectionModel.consume()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||||
if (action == null) {
|
if (action == null) {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
|
@ -237,7 +264,6 @@ class MainFragment :
|
||||||
when (action) {
|
when (action) {
|
||||||
is MainNavigationAction.Expand -> tryExpandSheets()
|
is MainNavigationAction.Expand -> tryExpandSheets()
|
||||||
is MainNavigationAction.Collapse -> tryCollapseSheets()
|
is MainNavigationAction.Collapse -> tryCollapseSheets()
|
||||||
// TODO: Figure out how to clear out the selections as one moves between screens.
|
|
||||||
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
|
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +309,6 @@ class MainFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
|
|
||||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
|
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
// Playback sheet is not expanded and not hidden, we can expand it.
|
// Playback sheet is not expanded and not hidden, we can expand it.
|
||||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
|
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
|
||||||
|
@ -294,7 +319,6 @@ class MainFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
|
|
||||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
|
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
|
||||||
// Make sure the queue is also collapsed here.
|
// Make sure the queue is also collapsed here.
|
||||||
val queueSheetBehavior =
|
val queueSheetBehavior =
|
||||||
|
@ -308,7 +332,6 @@ class MainFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
|
|
||||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
|
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||||
val queueSheetBehavior =
|
val queueSheetBehavior =
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
|
@ -328,13 +351,11 @@ class MainFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
|
|
||||||
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
|
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||||
val queueSheetBehavior =
|
val queueSheetBehavior =
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
|
|
||||||
// Make these views non-draggable so the user can't halt the hiding event.
|
// Make both bottom sheets non-draggable so the user can't halt the hiding event.
|
||||||
|
|
||||||
queueSheetBehavior?.apply {
|
queueSheetBehavior?.apply {
|
||||||
isDraggable = false
|
isDraggable = false
|
||||||
state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
|
@ -396,10 +396,12 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
||||||
* @param context [Context] required for [resolveName]. TODO Internationalize the list
|
* @param context [Context] required for [resolveName].
|
||||||
* formatter.
|
* formatter.
|
||||||
*/
|
*/
|
||||||
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
fun resolveArtistContents(context: Context) =
|
||||||
|
// TODO Internationalize the list
|
||||||
|
artists.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only
|
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only
|
||||||
|
@ -612,9 +614,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The earliest [Date] this album was released. Will be null if no valid date was present in the
|
* The earliest [Date] this album was released. Will be null if no valid date was present in the
|
||||||
* metadata of any [Song]. TODO: Date ranges?
|
* metadata of any [Song]
|
||||||
*/
|
*/
|
||||||
val date: Date?
|
val date: Date? // TODO: Date ranges?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Type] of this album, signifying the type of release it actually is. Defaults to
|
* The [Type] of this album, signifying the type of release it actually is. Defaults to
|
||||||
|
|
|
@ -122,13 +122,13 @@ class MetadataExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO:
|
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed.
|
||||||
* Re-unify with MetadataExtractor.
|
|
||||||
* @param context [Context] required to open the audio file.
|
* @param context [Context] required to open the audio file.
|
||||||
* @param raw [Song.Raw] to process.
|
* @param raw [Song.Raw] to process.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Task(context: Context, private val raw: Song.Raw) {
|
class Task(context: Context, private val raw: Song.Raw) {
|
||||||
|
// TODO: Unify with MetadataExtractor
|
||||||
// Note that we do not leverage future callbacks. This is because errors in the
|
// Note that we do not leverage future callbacks. This is because errors in the
|
||||||
// (highly fallible) extraction process will not bubble up to Indexer when a
|
// (highly fallible) extraction process will not bubble up to Indexer when a
|
||||||
// listener is used, instead crashing the app entirely.
|
// listener is used, instead crashing the app entirely.
|
||||||
|
|
|
@ -30,10 +30,11 @@ import org.oxycblt.auxio.util.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
|
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
|
||||||
* split tags with multiple values. TODO: Add saved state for pending configurations.
|
* split tags with multiple values.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||||
|
// TODO: Add saved state for pending configurations.
|
||||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
|
|
@ -27,13 +27,14 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a [ViewModel] that manages the current music picker state. TODO: This really shouldn't exist.
|
* a [ViewModel] that manages the current music picker state.
|
||||||
* Make it so that the dialogs just contain the music themselves and then exit if the library
|
* Make it so that the dialogs just contain the music themselves and then exit if the library
|
||||||
* changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
|
* changes.
|
||||||
* this dialog.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class PickerViewModel : ViewModel(), MusicStore.Callback {
|
class PickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
// TODO: Refactor
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val _currentSong = MutableStateFlow<Song?>(null)
|
private val _currentSong = MutableStateFlow<Song?>(null)
|
||||||
|
|
|
@ -122,15 +122,14 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the configuration for specific directories to filter to/from when loading music. TODO:
|
* Represents the configuration for specific directories to filter to/from when loading music.
|
||||||
* Migrate to a combined "Include + Exclude" system that is more sensible.
|
|
||||||
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
|
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
|
||||||
* .
|
|
||||||
* @param shouldInclude True if the library should only load from the [Directory] instances, false
|
* @param shouldInclude True if the library should only load from the [Directory] instances, false
|
||||||
* if the library should not load from the [Directory] instances.
|
* if the library should not load from the [Directory] instances.
|
||||||
* @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.
|
||||||
|
|
|
@ -511,9 +511,10 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version-compatible identifier for the read external storage permission required by the
|
* A version-compatible identifier for the read external storage permission required by the
|
||||||
* system to load audio. TODO: Move elsewhere.
|
* system to load audio.
|
||||||
*/
|
*/
|
||||||
val PERMISSION_READ_AUDIO =
|
val PERMISSION_READ_AUDIO =
|
||||||
|
// TODO: Move elsewhere.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
// READ_EXTERNAL_STORAGE was superseded by READ_MEDIA_AUDIO in Android 13
|
// READ_EXTERNAL_STORAGE was superseded by READ_MEDIA_AUDIO in Android 13
|
||||||
Manifest.permission.READ_MEDIA_AUDIO
|
Manifest.permission.READ_MEDIA_AUDIO
|
||||||
|
|
|
@ -164,7 +164,11 @@ class PlaybackPanelFragment : ViewBindingFragment<FragmentPlaybackPanelBinding>(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun updateSong(song: Song?) {
|
||||||
if (song == null) return
|
if (song == null) {
|
||||||
|
// Nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.playbackCover.bind(song)
|
binding.playbackCover.bind(song)
|
||||||
|
|
|
@ -34,7 +34,7 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ViewModel that provides a safe UI frontend for [PlaybackStateManager].
|
* An [AndroidViewModel] that provides a safe UI frontend for the current playback state.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class PlaybackViewModel(application: Application) :
|
class PlaybackViewModel(application: Application) :
|
||||||
|
@ -51,7 +51,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||||
val parent: StateFlow<MusicParent?> = _parent
|
val parent: StateFlow<MusicParent?> = _parent
|
||||||
private val _isPlaying = MutableStateFlow(false)
|
private val _isPlaying = MutableStateFlow(false)
|
||||||
/** Whether playback is ongoing or paused.*/
|
/** Whether playback is ongoing or paused. */
|
||||||
val isPlaying: StateFlow<Boolean>
|
val isPlaying: StateFlow<Boolean>
|
||||||
get() = _isPlaying
|
get() = _isPlaying
|
||||||
private val _positionDs = MutableStateFlow(0L)
|
private val _positionDs = MutableStateFlow(0L)
|
||||||
|
@ -103,10 +103,10 @@ class PlaybackViewModel(application: Application) :
|
||||||
|
|
||||||
override fun onStateChanged(state: InternalPlayer.State) {
|
override fun onStateChanged(state: InternalPlayer.State) {
|
||||||
_isPlaying.value = state.isPlaying
|
_isPlaying.value = state.isPlaying
|
||||||
|
// Still need to update the position now due to co-routine launch delays
|
||||||
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
|
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
|
||||||
|
// Replace the previous position co-routine with a new one that uses the new
|
||||||
// Cancel the previous position job relying on old state information and create
|
// state information.
|
||||||
// a new one.
|
|
||||||
lastPositionJob?.cancel()
|
lastPositionJob?.cancel()
|
||||||
lastPositionJob =
|
lastPositionJob =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -143,6 +143,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a [Song] from it's [Album].
|
* Play a [Song] from it's [Album].
|
||||||
|
* @param song The [Song] to play.
|
||||||
*/
|
*/
|
||||||
fun playFromAlbum(song: Song) {
|
fun playFromAlbum(song: Song) {
|
||||||
playbackManager.play(song, song.album, settings)
|
playbackManager.play(song, song.album, settings)
|
||||||
|
|
Loading…
Reference in a new issue