Add playing song nav to PlaybackFragment

Add the ability to nav to the playing song/playing artist/playing album from PlaybackFragment.
This commit is contained in:
OxygenCobalt 2020-11-25 09:37:06 -07:00
parent 6ce8c854a9
commit 206d8d1c1f
16 changed files with 173 additions and 37 deletions

View file

@ -83,14 +83,14 @@ dependencies {
// Material // Material
implementation 'com.google.android.material:material:1.3.0-alpha03' implementation 'com.google.android.material:material:1.3.0-alpha03'
// Lint
ktlint "com.pinterest:ktlint:0.37.2"
// ExoPlayer // ExoPlayer
def exoplayer_version = "2.12.1" def exoplayer_version = "2.12.1"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:extension-mediasession:$exoplayer_version" implementation "com.google.android.exoplayer:extension-mediasession:$exoplayer_version"
// Lint
ktlint "com.pinterest:ktlint:0.37.2"
// Memory Leak checking // Memory Leak checking
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
} }

View file

@ -59,9 +59,9 @@ class MainFragment : Fragment() {
) )
val navController = ( val navController = (
childFragmentManager.findFragmentById(R.id.explore_nav_host) childFragmentManager.findFragmentById(R.id.explore_nav_host)
as NavHostFragment? as NavHostFragment?
)?.findNavController() )?.findNavController()
// --- UI SETUP --- // --- UI SETUP ---
@ -93,17 +93,30 @@ class MainFragment : Fragment() {
} }
} }
playbackModel.navToSong.observe(viewLifecycleOwner) { playbackModel.navToPlayingSong.observe(viewLifecycleOwner) {
if (it) { if (it) {
if (binding.navBar.selectedItemId != R.id.library_fragment || if (binding.navBar.selectedItemId != R.id.library_fragment ||
( shouldGoToAlbum(navController!!)
navController!!.currentDestination?.id == R.id.album_detail_fragment && ) {
detailModel.currentAlbum.value == null || binding.navBar.selectedItemId = R.id.library_fragment
detailModel.currentAlbum.value?.id }
!= playbackModel.song.value!!.album.id }
) || }
navController.currentDestination?.id == R.id.artist_detail_fragment ||
navController.currentDestination?.id == R.id.genre_detail_fragment playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) {
if (it) {
if (binding.navBar.selectedItemId != R.id.library_fragment ||
shouldGoToAlbum(navController!!)
) {
binding.navBar.selectedItemId = R.id.library_fragment
}
}
}
playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) {
if (it) {
if (binding.navBar.selectedItemId != R.id.library_fragment ||
shouldGoToArtist(navController!!)
) { ) {
binding.navBar.selectedItemId = R.id.library_fragment binding.navBar.selectedItemId = R.id.library_fragment
} }
@ -117,6 +130,25 @@ class MainFragment : Fragment() {
return binding.root return binding.root
} }
// I have no idea what these things even do
private fun shouldGoToAlbum(controller: NavController): Boolean {
return (
controller.currentDestination!!.id == R.id.album_detail_fragment &&
detailModel.currentAlbum.value?.id != playbackModel.song.value!!.album.id
) ||
controller.currentDestination!!.id == R.id.artist_detail_fragment ||
controller.currentDestination!!.id == R.id.genre_detail_fragment
}
private fun shouldGoToArtist(controller: NavController): Boolean {
return (
controller.currentDestination!!.id == R.id.artist_detail_fragment &&
detailModel.currentArtist.value?.id != playbackModel.song.value!!.album.artist.id
) ||
controller.currentDestination!!.id == R.id.album_detail_fragment ||
controller.currentDestination!!.id == R.id.genre_detail_fragment
}
private fun navigateWithItem(navController: NavController, item: MenuItem): Boolean { private fun navigateWithItem(navController: NavController, item: MenuItem): Boolean {
if (navController.currentDestination!!.id != item.itemId) { if (navController.currentDestination!!.id != item.itemId) {
// Create custom NavOptions myself so that animations work // Create custom NavOptions myself so that animations work

View file

@ -1,5 +1,17 @@
package org.oxycblt.auxio.database package org.oxycblt.auxio.database
/**
* A database entity that stores a compressed variant of the current playback state.
* @property id - The database key for this state
* @property songId - The song that is currently playing
* @property parentId - The parent that is being played from [-1 if none]
* @property index - The current index in the queue.
* @property mode - The integer form of the current [org.oxycblt.auxio.playback.state.PlaybackMode]
* @property isShuffling - A bool for if the queue was shuffled
* @property shuffleSeed - A long for the seed used to shuffle the queue [Used for quick-restore]
* @property loopMode - The integer form of the current [org.oxycblt.auxio.playback.state.LoopMode]
* @property inUserQueue - A bool for if the state was currently playing from the user queue.
*/
data class PlaybackState( data class PlaybackState(
val id: Long = 0L, val id: Long = 0L,
val songId: Long = -1L, val songId: Long = -1L,

View file

@ -188,6 +188,7 @@ class PlaybackStateDatabase(context: Context) :
var position = 0 var position = 0
// Try to write out the entirety of the queue, any failed inserts will be skipped.
while (position < queue.size) { while (position < queue.size) {
database.beginTransaction() database.beginTransaction()
var i = position var i = position

View file

@ -1,5 +1,12 @@
package org.oxycblt.auxio.database package org.oxycblt.auxio.database
/**
* A database entity that stores a simplified representation of a song in a queue.
* @property id The database entity's id
* @property songId The song id for this queue item
* @property albumId The album id for this queue item, used to make searching quicker
* @property isUserQueue A bool for if this queue item is a user queue item or not
*/
data class QueueItem( data class QueueItem(
var id: Long = 0L, var id: Long = 0L,
val songId: Long = Long.MIN_VALUE, val songId: Long = Long.MIN_VALUE,

View file

@ -122,18 +122,20 @@ class AlbumDetailFragment : DetailFragment() {
} }
} }
playbackModel.navToSong.observe(viewLifecycleOwner) { playbackModel.navToPlayingSong.observe(viewLifecycleOwner) {
if (it) { if (it) {
// Calculate where the item for the currently played song is, and navigate to there. // Calculate where the item for the currently played song is, and navigate to there.
val pos = detailModel.currentAlbum.value!!.songs.indexOf(playbackModel.song.value) val pos = detailModel.albumSortMode.value!!.getSortedSongList(
detailModel.currentAlbum.value!!.songs
).indexOf(playbackModel.song.value)
if (pos != -1) { if (pos != -1) {
binding.albumSongRecycler.post { binding.albumSongRecycler.post {
// Only scroll after UI creation // Only scroll after UI creation
val y = binding.albumSongRecycler.y + val y = binding.albumSongRecycler.y +
binding.albumSongRecycler.getChildAt(pos).y binding.albumSongRecycler.getChildAt(pos).y
binding.nestedScroll.smoothScrollBy(0, y.toInt()) binding.nestedScroll.smoothScrollTo(0, y.toInt())
} }
playbackModel.doneWithNavToPlayingSong() playbackModel.doneWithNavToPlayingSong()
@ -141,6 +143,12 @@ class AlbumDetailFragment : DetailFragment() {
} }
} }
playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) {
if (it) {
playbackModel.doneWithNavToPlayingAlbum()
}
}
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
return binding.root return binding.root

View file

@ -108,6 +108,12 @@ class ArtistDetailFragment : DetailFragment() {
) )
} }
playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) {
if (it) {
playbackModel.doneWithNavToPlayingArtist()
}
}
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
return binding.root return binding.root

View file

@ -151,7 +151,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
} }
playbackModel.navToSong.observe(viewLifecycleOwner) { playbackModel.navToPlayingSong.observe(viewLifecycleOwner) {
if (it) { if (it) {
libraryModel.updateNavigationStatus(false) libraryModel.updateNavigationStatus(false)
@ -159,6 +159,22 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
} }
playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) {
if (it) {
libraryModel.updateNavigationStatus(false)
navToItem(playbackModel.song.value!!.album)
}
}
playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) {
if (it) {
libraryModel.updateNavigationStatus(false)
navToItem(playbackModel.song.value!!.album.artist)
}
}
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
return binding.root return binding.root

View file

@ -69,6 +69,9 @@ data class Album(
/** /**
* The data object for an artist. Inherits [BaseModel] * The data object for an artist. Inherits [BaseModel]
* @property albums The list of all [Album]s in this artist
* @property genres The list of all parent [Genre]s in this artist, sorted by relevance
* @property songs The list of all [Song]s in this artist
* @author OxygenCobalt * @author OxygenCobalt
*/ */
data class Artist( data class Artist(
@ -89,7 +92,8 @@ data class Artist(
/** /**
* The data object for a genre. Inherits [BaseModel] * The data object for a genre. Inherits [BaseModel]
* @property artists The list of all [Artist]s in this genre * @property artists The list of all [Artist]s in this genre.
* @property albums The list of all [Album]s in this genre.
* @property songs The list of all [Song]s in this genre. * @property songs The list of all [Song]s in this genre.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@ -117,6 +121,8 @@ data class Genre(
/** /**
* A data object used solely for the "Header" UI element. Inherits [BaseModel]. * A data object used solely for the "Header" UI element. Inherits [BaseModel].
* @property isAction Value that marks whether this header should have an action attached to it.
* @author OxygenCobalt
*/ */
data class Header( data class Header(
override val id: Long = -1, override val id: Long = -1,

View file

@ -9,7 +9,7 @@ import org.oxycblt.auxio.music.processing.MusicSorter
import org.oxycblt.auxio.recycler.ShowMode import org.oxycblt.auxio.recycler.ShowMode
/** /**
* The main storage for music items. Use [MusicStore.from()] to get the instance. * The main storage for music items. Use [MusicStore.getInstance] to get the instance.
*/ */
class MusicStore private constructor() { class MusicStore private constructor() {
private var mGenres = listOf<Genre>() private var mGenres = listOf<Genre>()

View file

@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
@ -54,10 +55,14 @@ class CompactPlaybackFragment : Fragment() {
true true
} }
binding.playbackControls.setOnLongClickListener { // Enable the ability to force-save the state in debug builds, in order to check
playbackModel.save(requireContext()) // for persistence issues without waiting for PlaybackService to be killed.
getString(R.string.debug_state_saved).createToast(requireContext()) if (BuildConfig.DEBUG) {
true binding.playbackControls.setOnLongClickListener {
playbackModel.save(requireContext())
getString(R.string.debug_state_saved).createToast(requireContext())
true
}
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---

View file

@ -187,6 +187,24 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
} }
} }
playbackModel.navToPlayingSong.observe(viewLifecycleOwner) {
if (it) {
findNavController().navigateUp()
}
}
playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) {
if (it) {
findNavController().navigateUp()
}
}
playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) {
if (it) {
findNavController().navigateUp()
}
}
Log.d(this::class.simpleName, "Fragment Created.") Log.d(this::class.simpleName, "Fragment Created.")
return binding.root return binding.root

View file

@ -62,8 +62,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val mIsSeeking = MutableLiveData(false) private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> get() = mIsSeeking val isSeeking: LiveData<Boolean> get() = mIsSeeking
private val mNavToSong = MutableLiveData(false) private val mNavToPlayingSong = MutableLiveData(false)
val navToSong: LiveData<Boolean> get() = mNavToSong val navToPlayingSong: LiveData<Boolean> get() = mNavToPlayingSong
private val mNavToPlayingAlbum = MutableLiveData(false)
val navToPlayingAlbum: LiveData<Boolean> get() = mNavToPlayingAlbum
private val mNavToPlayingArtist = MutableLiveData(false)
val navToPlayingArtist: LiveData<Boolean> get() = mNavToPlayingArtist
private var mCanAnimate = false private var mCanAnimate = false
val canAnimate: Boolean get() = mCanAnimate val canAnimate: Boolean get() = mCanAnimate
@ -283,11 +289,27 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
fun navToPlayingSong() { fun navToPlayingSong() {
mNavToSong.value = true mNavToPlayingSong.value = true
} }
fun doneWithNavToPlayingSong() { fun doneWithNavToPlayingSong() {
mNavToSong.value = false mNavToPlayingSong.value = false
}
fun navToPlayingAlbum() {
mNavToPlayingAlbum.value = true
}
fun doneWithNavToPlayingAlbum() {
mNavToPlayingAlbum.value = false
}
fun navToPlayingArtist() {
mNavToPlayingArtist.value = true
}
fun doneWithNavToPlayingArtist() {
mNavToPlayingArtist.value = false
} }
fun enableAnimation() { fun enableAnimation() {

View file

@ -22,7 +22,7 @@ import kotlin.random.Random
* - If you want to use the playback state with the ExoPlayer instance or system-side things, * - If you want to use the playback state with the ExoPlayer instance or system-side things,
* use [org.oxycblt.auxio.playback.PlaybackService]. * use [org.oxycblt.auxio.playback.PlaybackService].
* *
* All instantiation should be done with [PlaybackStateManager.from()]. * All instantiation should be done with [PlaybackStateManager.getInstance].
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackStateManager private constructor() { class PlaybackStateManager private constructor() {
@ -560,12 +560,12 @@ class PlaybackStateManager private constructor() {
// Traverse albums and then album songs instead of just the songs, as its faster. // Traverse albums and then album songs instead of just the songs, as its faster.
musicStore.albums.find { it.id == item.albumId } musicStore.albums.find { it.id == item.albumId }
?.songs?.find { it.id == item.songId }?.let { ?.songs?.find { it.id == item.songId }?.let {
if (item.isUserQueue) { if (item.isUserQueue) {
mUserQueue.add(it) mUserQueue.add(it)
} else { } else {
mQueue.add(it) mQueue.add(it)
}
} }
}
} }
// Get a more accurate index [At least if were not in the user queue] // Get a more accurate index [At least if were not in the user queue]

View file

@ -9,7 +9,7 @@ https://stackoverflow.com/a/61157571/14143986
android:top="-2dp"> android:top="-2dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<stroke <stroke
android:width="1dp" android:width="0.5dp"
android:color="@color/divider_color" /> android:color="@color/divider_color" />
</shape> </shape>
</item> </item>

View file

@ -67,6 +67,7 @@
android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginStart="@dimen/margin_mid_large"
android:layout_marginEnd="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large"
android:text="@{song.name}" android:text="@{song.name}"
android:onClick="@{() -> playbackModel.navToPlayingSong()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintBottom_toTopOf="@+id/playback_artist" app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -83,6 +84,7 @@
android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginStart="@dimen/margin_mid_large"
android:layout_marginEnd="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large"
android:text="@{song.album.artist.name}" android:text="@{song.album.artist.name}"
android:onClick="@{() -> playbackModel.navToPlayingArtist()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/playback_album" app:layout_constraintBottom_toTopOf="@+id/playback_album"
@ -100,6 +102,7 @@
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:text="@{song.album.name}" android:text="@{song.album.name}"
android:onClick="@{() -> playbackModel.navToPlayingAlbum()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"