diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index b0e21dc60..394e6fff9 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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(), ViewTreeObserver.OnPreDrawListener { + ViewBindingFragment(), 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 3e677ff40..d13012e17 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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) : 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index f9197cb34..98f58c530 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index e060a76aa..fde4b7933 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -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() { + // TODO: Add saved state for pending configurations. private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } override fun onCreateBinding(inflater: LayoutInflater) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index b2cb7877b..e56516ae3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -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(null) diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt index 00d33959b..16424a06d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt @@ -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, val shouldInclude: Boolean) +// TODO: Unify include + exclude /** * A mime type of a file. Only intended for display. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index bf10edd99..7ebb1d24f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 982bf5209..f192dd013 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -164,7 +164,11 @@ class PlaybackPanelFragment : ViewBindingFragment( } 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) 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 72b294403..7871da9ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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 = _parent private val _isPlaying = MutableStateFlow(false) - /** Whether playback is ongoing or paused.*/ + /** Whether playback is ongoing or paused. */ val isPlaying: StateFlow 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)