Add queue display
Add a fragment for displaying the current queue.
This commit is contained in:
parent
3337747cb7
commit
219d1c2c24
14 changed files with 202 additions and 24 deletions
|
@ -2,6 +2,7 @@ package org.oxycblt.auxio.music.coil
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import coil.Coil
|
||||
|
@ -79,17 +80,30 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
|||
if (genre.numArtists >= 4) {
|
||||
val uris = mutableListOf<Uri>()
|
||||
|
||||
// For each artist, get the nth album from them [if possible].
|
||||
for (i in 0..3) {
|
||||
val artist = genre.artists[i]
|
||||
Log.d(this::class.simpleName, genre.numAlbums.toString())
|
||||
|
||||
uris.add(
|
||||
if (artist.albums.size > i) {
|
||||
artist.albums[i].coverUri
|
||||
} else {
|
||||
artist.albums[0].coverUri
|
||||
// Try to create a 4x4 mosaic if possible, if not, just create a 2x2 mosaic.
|
||||
if (genre.numAlbums >= 16) {
|
||||
while (uris.size < 16) {
|
||||
genre.artists.forEach { artist ->
|
||||
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)
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.io.InputStream
|
|||
const val MOSAIC_BITMAP_SIZE = 512
|
||||
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>> {
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
|
|
|
@ -20,15 +20,7 @@ class MusicSorter(
|
|||
sortSongsIntoAlbums()
|
||||
sortAlbumsIntoArtists()
|
||||
sortArtistsIntoGenres()
|
||||
|
||||
// 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() }
|
||||
fixBuggyGenres()
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.fragment.app.activityViewModels
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
|
||||
import org.oxycblt.auxio.playback.queue.QueueFragment
|
||||
import org.oxycblt.auxio.theme.accent
|
||||
import org.oxycblt.auxio.theme.disable
|
||||
import org.oxycblt.auxio.theme.enable
|
||||
|
@ -50,8 +51,18 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
binding.playbackModel = playbackModel
|
||||
binding.song = playbackModel.currentSong.value!!
|
||||
|
||||
binding.playbackToolbar.setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
binding.playbackToolbar.apply {
|
||||
setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
if (it.itemId == R.id.action_queue) {
|
||||
QueueFragment().show(parentFragmentManager, "TAG_QUEUE")
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Make marquee scroll work
|
||||
|
|
|
@ -62,11 +62,16 @@ class PlaybackViewModel : ViewModel() {
|
|||
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.
|
||||
fun update(song: Song, mode: PlaybackMode) {
|
||||
|
||||
// Auxio doesn't support playing songs while swapping the mode to GENRE, as genres
|
||||
// are bound to artists, not songs.
|
||||
// Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible
|
||||
// to determine what genre a song has.
|
||||
if (mode == PlaybackMode.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
|
||||
}
|
||||
|
||||
// Skip to next song
|
||||
fun skipNext() {
|
||||
if (mCurrentIndex.value!! < mQueue.value!!.size) {
|
||||
mCurrentIndex.value = mCurrentIndex.value!!.inc()
|
||||
}
|
||||
|
||||
updatePlayback(mQueue.value!![mCurrentIndex.value!!])
|
||||
|
||||
// Force the observers to actually update.
|
||||
mQueue.value = mQueue.value
|
||||
}
|
||||
|
||||
// Skip to last song
|
||||
fun skipPrev() {
|
||||
if (mCurrentIndex.value!! > 0) {
|
||||
mCurrentIndex.value = mCurrentIndex.value!!.dec()
|
||||
}
|
||||
|
||||
updatePlayback(mQueue.value!![mCurrentIndex.value!!])
|
||||
|
||||
// Force the observers to actually update.
|
||||
mQueue.value = mQueue.value
|
||||
}
|
||||
|
||||
fun resetAnimStatus() {
|
||||
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) {
|
||||
mCurrentSong.value = song
|
||||
mCurrentDuration.value = 0
|
||||
|
@ -275,8 +290,12 @@ class PlaybackViewModel : ViewModel() {
|
|||
// Otherwise, just start from the zeroth position in the queue.
|
||||
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() {
|
||||
mShuffleSeed.value = -1
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
11
app/src/main/res/drawable/ic_queue.xml
Normal file
11
app/src/main/res/drawable/ic_queue.xml
Normal 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>
|
|
@ -20,6 +20,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="@color/background">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
|
@ -34,6 +35,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:title="@string/title_playback"
|
||||
app:menu="@menu/menu_playback"
|
||||
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
|
||||
|
||||
<ImageView
|
||||
|
|
30
app/src/main/res/layout/fragment_queue.xml
Normal file
30
app/src/main/res/layout/fragment_queue.xml
Normal 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>
|
9
app/src/main/res/menu/menu_playback.xml
Normal file
9
app/src/main/res/menu/menu_playback.xml
Normal 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>
|
|
@ -14,7 +14,6 @@
|
|||
<dimen name="margin_mid_small">10dp</dimen>
|
||||
<dimen name="margin_medium">16dp</dimen>
|
||||
<dimen name="margin_mid_large">24dp</dimen>
|
||||
<dimen name="margin_play">26dp</dimen>
|
||||
<dimen name="margin_large">32dp</dimen>
|
||||
<dimen name="margin_huge">64dp</dimen>
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
<string name="label_sort_alpha_up">Z-A</string>
|
||||
<string name="label_shuffle">Shuffle</string>
|
||||
<string name="label_play">Play</string>
|
||||
<string name="label_queue">Queue</string>
|
||||
|
||||
<!-- Hint Namespace | EditText Hints -->
|
||||
<string name="hint_search_library">Search Library…</string>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<item name="android:fontFamily">@font/inter</item>
|
||||
<item name="android:textCursorDrawable">@drawable/ui_cursor</item>
|
||||
<item name="android:colorControlNormal">@color/control_color</item>
|
||||
<item name="android:fitsSystemWindows">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Toolbar Themes -->
|
||||
|
@ -36,4 +37,14 @@
|
|||
<item name="android:colorBackground">@color/background</item>
|
||||
<item name="colorControlHighlight">@color/selection_color</item>
|
||||
</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>
|
Loading…
Reference in a new issue