Add queue display

Add a fragment for displaying the current queue.
This commit is contained in:
OxygenCobalt 2020-10-20 15:13:32 -06:00
parent 3337747cb7
commit 219d1c2c24
14 changed files with 202 additions and 24 deletions

View file

@ -2,6 +2,7 @@ package org.oxycblt.auxio.music.coil
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import coil.Coil import coil.Coil
@ -79,17 +80,30 @@ fun ImageView.bindGenreImage(genre: Genre) {
if (genre.numArtists >= 4) { if (genre.numArtists >= 4) {
val uris = mutableListOf<Uri>() val uris = mutableListOf<Uri>()
// For each artist, get the nth album from them [if possible]. Log.d(this::class.simpleName, genre.numAlbums.toString())
for (i in 0..3) {
val artist = genre.artists[i]
uris.add( // Try to create a 4x4 mosaic if possible, if not, just create a 2x2 mosaic.
if (artist.albums.size > i) { if (genre.numAlbums >= 16) {
artist.albums[i].coverUri while (uris.size < 16) {
} else { genre.artists.forEach { artist ->
artist.albums[0].coverUri artist.albums.forEach {
uris.add(it.coverUri)
}
} }
) }
} else {
// Get the Nth cover from each artist, if possible.
for (i in 0..3) {
val artist = genre.artists[i]
uris.add(
if (artist.albums.size > i) {
artist.albums[i].coverUri
} else {
artist.albums[0].coverUri
}
)
}
} }
val fetcher = MosaicFetcher(context) val fetcher = MosaicFetcher(context)

View file

@ -22,7 +22,7 @@ import java.io.InputStream
const val MOSAIC_BITMAP_SIZE = 512 const val MOSAIC_BITMAP_SIZE = 512
const val MOSAIC_BITMAP_INCREMENT = 256 const val MOSAIC_BITMAP_INCREMENT = 256
// A Fetcher that takes multiple cover uris and turns them into a 2x2 mosaic image. // A Fetcher that takes multiple cover uris and turns them into a NxN mosaic image.
class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> { class MosaicFetcher(private val context: Context) : Fetcher<List<Uri>> {
override suspend fun fetch( override suspend fun fetch(
pool: BitmapPool, pool: BitmapPool,

View file

@ -20,15 +20,7 @@ class MusicSorter(
sortSongsIntoAlbums() sortSongsIntoAlbums()
sortAlbumsIntoArtists() sortAlbumsIntoArtists()
sortArtistsIntoGenres() sortArtistsIntoGenres()
fixBuggyGenres()
// Remove genre duplicates at the end, as duplicate genres can be added during
// the sorting process as well.
genres = genres.distinctBy {
it.name
}.toMutableList()
// Also elimate any genres that dont have artists, which also happens sometimes.
genres.removeAll { it.artists.isEmpty() }
} }
private fun sortSongsIntoAlbums() { private fun sortSongsIntoAlbums() {
@ -161,4 +153,16 @@ class MusicSorter(
) )
} }
} }
// Band-aid any buggy genres created by the broken Music Loading system.
private fun fixBuggyGenres() {
// Remove genre duplicates at the end, as duplicate genres can be added during
// the sorting process as well.
genres = genres.distinctBy {
it.name
}.toMutableList()
// Also eliminate any genres that don't have artists, which also happens sometimes.
genres.removeAll { it.artists.isEmpty() }
}
} }

View file

@ -14,6 +14,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.playback.queue.QueueFragment
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.theme.disable
import org.oxycblt.auxio.theme.enable import org.oxycblt.auxio.theme.enable
@ -50,8 +51,18 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
binding.playbackModel = playbackModel binding.playbackModel = playbackModel
binding.song = playbackModel.currentSong.value!! binding.song = playbackModel.currentSong.value!!
binding.playbackToolbar.setNavigationOnClickListener { binding.playbackToolbar.apply {
findNavController().navigateUp() setNavigationOnClickListener {
findNavController().navigateUp()
}
setOnMenuItemClickListener {
if (it.itemId == R.id.action_queue) {
QueueFragment().show(parentFragmentManager, "TAG_QUEUE")
}
true
}
} }
// Make marquee scroll work // Make marquee scroll work

View file

@ -62,11 +62,16 @@ class PlaybackViewModel : ViewModel() {
if (mCurrentSong.value != null) it.toInt() else 0 if (mCurrentSong.value != null) it.toInt() else 0
} }
// Formatted queue that shows all the songs after the current playing song.
val formattedQueue = Transformations.map(mQueue) {
it.slice((mCurrentIndex.value!! + 1) until it.size)
}
// Update the current song while changing the queue mode. // Update the current song while changing the queue mode.
fun update(song: Song, mode: PlaybackMode) { fun update(song: Song, mode: PlaybackMode) {
// Auxio doesn't support playing songs while swapping the mode to GENRE, as genres // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible
// are bound to artists, not songs. // to determine what genre a song has.
if (mode == PlaybackMode.IN_GENRE) { if (mode == PlaybackMode.IN_GENRE) {
Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.") Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.")
@ -224,26 +229,36 @@ class PlaybackViewModel : ViewModel() {
mIsSeeking.value = status mIsSeeking.value = status
} }
// Skip to next song
fun skipNext() { fun skipNext() {
if (mCurrentIndex.value!! < mQueue.value!!.size) { if (mCurrentIndex.value!! < mQueue.value!!.size) {
mCurrentIndex.value = mCurrentIndex.value!!.inc() mCurrentIndex.value = mCurrentIndex.value!!.inc()
} }
updatePlayback(mQueue.value!![mCurrentIndex.value!!]) updatePlayback(mQueue.value!![mCurrentIndex.value!!])
// Force the observers to actually update.
mQueue.value = mQueue.value
} }
// Skip to last song
fun skipPrev() { fun skipPrev() {
if (mCurrentIndex.value!! > 0) { if (mCurrentIndex.value!! > 0) {
mCurrentIndex.value = mCurrentIndex.value!!.dec() mCurrentIndex.value = mCurrentIndex.value!!.dec()
} }
updatePlayback(mQueue.value!![mCurrentIndex.value!!]) updatePlayback(mQueue.value!![mCurrentIndex.value!!])
// Force the observers to actually update.
mQueue.value = mQueue.value
} }
fun resetAnimStatus() { fun resetAnimStatus() {
mCanAnimate = false mCanAnimate = false
} }
// Generic function for updating the playback with a new song.
// Use this instead of manually updating the values each time.
private fun updatePlayback(song: Song) { private fun updatePlayback(song: Song) {
mCurrentSong.value = song mCurrentSong.value = song
mCurrentDuration.value = 0 mCurrentDuration.value = 0
@ -275,8 +290,12 @@ class PlaybackViewModel : ViewModel() {
// Otherwise, just start from the zeroth position in the queue. // Otherwise, just start from the zeroth position in the queue.
mCurrentSong.value = mQueue.value!![0] mCurrentSong.value = mQueue.value!![0]
} }
// Force the observers to actually update.
mQueue.value = mQueue.value
} }
// Stop the queue and attempt to restore to the previous state
private fun resetShuffle() { private fun resetShuffle() {
mShuffleSeed.value = -1 mShuffleSeed.value = -1

View file

@ -0,0 +1,20 @@
package org.oxycblt.auxio.playback.queue
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
class QueueAdapter(
private val doOnClick: (Song) -> Unit
) : ListAdapter<Song, SongViewHolder>(DiffCallback<Song>()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
return SongViewHolder.from(parent.context, doOnClick)
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
holder.bind(getItem(position))
}
}

View file

@ -0,0 +1,47 @@
package org.oxycblt.auxio.playback.queue
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.toColor
class QueueFragment : BottomSheetDialogFragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
override fun getTheme(): Int = R.style.Theme_BottomSheetFix
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentQueueBinding.inflate(inflater)
val queueAdapter = QueueAdapter {}
// --- UI SETUP ---
binding.queueHeader.setTextColor(accent.first.toColor(requireContext()))
binding.queueRecycler.apply {
adapter = queueAdapter
applyDivider()
setHasFixedSize(true)
}
// --- VIEWMODEL SETUP ---
playbackModel.formattedQueue.observe(viewLifecycleOwner) {
queueAdapter.submitList(it)
}
return binding.root
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15,6L3,6v2h12L15,6zM15,10L3,10v2h12v-2zM3,16h8v-2L3,14v2zM17,6v8.18c-0.31,-0.11 -0.65,-0.18 -1,-0.18 -1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3L19,8h3L22,6h-5z" />
</vector>

View file

@ -20,6 +20,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:fitsSystemWindows="true"
android:background="@color/background"> android:background="@color/background">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
@ -34,6 +35,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:title="@string/title_playback" app:title="@string/title_playback"
app:menu="@menu/menu_playback"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" /> app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
<ImageView <ImageView

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:theme="@style/Theme.Base"
android:background="@color/background">
<TextView
android:id="@+id/queue_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_medium"
android:text="@string/label_queue"
android:fontFamily="@font/inter_black"
android:textAppearance="@style/TextAppearance.Toolbar.Header" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_song"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_queue"
android:icon="@drawable/ic_queue"
android:title="@string/label_queue"
app:showAsAction="always" />
</menu>

View file

@ -14,7 +14,6 @@
<dimen name="margin_mid_small">10dp</dimen> <dimen name="margin_mid_small">10dp</dimen>
<dimen name="margin_medium">16dp</dimen> <dimen name="margin_medium">16dp</dimen>
<dimen name="margin_mid_large">24dp</dimen> <dimen name="margin_mid_large">24dp</dimen>
<dimen name="margin_play">26dp</dimen>
<dimen name="margin_large">32dp</dimen> <dimen name="margin_large">32dp</dimen>
<dimen name="margin_huge">64dp</dimen> <dimen name="margin_huge">64dp</dimen>

View file

@ -25,6 +25,7 @@
<string name="label_sort_alpha_up">Z-A</string> <string name="label_sort_alpha_up">Z-A</string>
<string name="label_shuffle">Shuffle</string> <string name="label_shuffle">Shuffle</string>
<string name="label_play">Play</string> <string name="label_play">Play</string>
<string name="label_queue">Queue</string>
<!-- Hint Namespace | EditText Hints --> <!-- Hint Namespace | EditText Hints -->
<string name="hint_search_library">Search Library…</string> <string name="hint_search_library">Search Library…</string>

View file

@ -7,6 +7,7 @@
<item name="android:fontFamily">@font/inter</item> <item name="android:fontFamily">@font/inter</item>
<item name="android:textCursorDrawable">@drawable/ui_cursor</item> <item name="android:textCursorDrawable">@drawable/ui_cursor</item>
<item name="android:colorControlNormal">@color/control_color</item> <item name="android:colorControlNormal">@color/control_color</item>
<item name="android:fitsSystemWindows">true</item>
</style> </style>
<!-- Toolbar Themes --> <!-- Toolbar Themes -->
@ -36,4 +37,14 @@
<item name="android:colorBackground">@color/background</item> <item name="android:colorBackground">@color/background</item>
<item name="colorControlHighlight">@color/selection_color</item> <item name="colorControlHighlight">@color/selection_color</item>
</style> </style>
<!--
Hack to get QueueFragment to not overlap the Status Bar or Navigation Bar
https://stackoverflow.com/a/57790787/14143986
-->
<style name="Theme.BottomSheetFix" parent="@style/Theme.Design.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:navigationBarColor">@color/background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources> </resources>