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:
Alexander Capehart 2022-12-26 19:08:08 -07:00
parent 0737dbace3
commit 7212700553
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 60 additions and 30 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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) =

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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) :
@ -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)