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.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) :
|
||||
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue