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.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
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.min
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.list.selection.SelectionViewModel
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
@ -51,11 +54,13 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class MainFragment :
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener {
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener, NavController.OnDestinationChangedListener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val callback = DynamicBackPressedCallback()
private var lastInsets: WindowInsets? = null
private var initialNavDestinationChange = true
private val elevationNormal: Float by lifecycleObject { binding ->
binding.context.getDimen(R.dimen.elevation_normal)
}
@ -128,14 +133,21 @@ class MainFragment :
override fun 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
// our pre-draw listener our listener in onStart/onStop respectively.
requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this)
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
}
override fun 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 {
@ -228,6 +240,21 @@ class MainFragment :
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?) {
if (action == null) {
// Nothing to do.
@ -237,7 +264,6 @@ class MainFragment :
when (action) {
is MainNavigationAction.Expand -> tryExpandSheets()
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)
}
@ -283,7 +309,6 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is not expanded and not hidden, we can expand it.
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
@ -294,7 +319,6 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
// Make sure the queue is also collapsed here.
val queueSheetBehavior =
@ -308,7 +332,6 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
@ -328,13 +351,11 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
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 {
isDraggable = false
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.
* @param context [Context] required for [resolveName]. TODO Internationalize the list
* @param context [Context] required for [resolveName].
* 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
@ -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
* 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

View file

@ -122,13 +122,13 @@ class MetadataExtractor(
}
/**
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO:
* Re-unify with MetadataExtractor.
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed.
* @param context [Context] required to open the audio file.
* @param raw [Song.Raw] to process.
* @author Alexander Capehart (OxygenCobalt)
*/
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
// (highly fallible) extraction process will not bubble up to Indexer when a
// 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
* split tags with multiple values. TODO: Add saved state for pending configurations.
* split tags with multiple values.
* @author Alexander Capehart (OxygenCobalt)
*/
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// TODO: Add saved state for pending configurations.
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) =

View file

@ -27,13 +27,14 @@ import org.oxycblt.auxio.music.Song
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
* changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
* this dialog.
* changes.
* @author Alexander Capehart (OxygenCobalt)
*/
class PickerViewModel : ViewModel(), MusicStore.Callback {
// TODO: Refactor
private val musicStore = MusicStore.getInstance()
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:
* Migrate to a combined "Include + Exclude" system that is more sensible.
* Represents the configuration for specific directories to filter to/from when loading music.
* @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
* if the library should not load from the [Directory] instances.
* @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

@ -511,9 +511,10 @@ class Indexer private constructor() {
/**
* 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 =
// TODO: Move elsewhere.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// READ_EXTERNAL_STORAGE was superseded by READ_MEDIA_AUDIO in Android 13
Manifest.permission.READ_MEDIA_AUDIO

View file

@ -164,7 +164,11 @@ class PlaybackPanelFragment : ViewBindingFragment<FragmentPlaybackPanelBinding>(
}
private fun updateSong(song: Song?) {
if (song == null) return
if (song == null) {
// Nothing to do.
return
}
val binding = requireBinding()
val context = requireContext()
binding.playbackCover.bind(song)

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.settings.Settings
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)
*/
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. */
val parent: StateFlow<MusicParent?> = _parent
private val _isPlaying = MutableStateFlow(false)
/** Whether playback is ongoing or paused.*/
/** Whether playback is ongoing or paused. */
val isPlaying: StateFlow<Boolean>
get() = _isPlaying
private val _positionDs = MutableStateFlow(0L)
@ -103,10 +103,10 @@ class PlaybackViewModel(application: Application) :
override fun onStateChanged(state: InternalPlayer.State) {
_isPlaying.value = state.isPlaying
// Still need to update the position now due to co-routine launch delays
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
// Cancel the previous position job relying on old state information and create
// a new one.
// Replace the previous position co-routine with a new one that uses the new
// state information.
lastPositionJob?.cancel()
lastPositionJob =
viewModelScope.launch {
@ -143,6 +143,7 @@ class PlaybackViewModel(application: Application) :
/**
* Play a [Song] from it's [Album].
* @param song The [Song] to play.
*/
fun playFromAlbum(song: Song) {
playbackManager.play(song, song.album, settings)