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.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,7 +80,19 @@ 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())
|
||||||
|
|
||||||
|
// 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) {
|
for (i in 0..3) {
|
||||||
val artist = genre.artists[i]
|
val artist = genre.artists[i]
|
||||||
|
|
||||||
|
@ -91,6 +104,7 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val fetcher = MosaicFetcher(context)
|
val fetcher = MosaicFetcher(context)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +51,20 @@ 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 {
|
||||||
|
setNavigationOnClickListener {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOnMenuItemClickListener {
|
||||||
|
if (it.itemId == R.id.action_queue) {
|
||||||
|
QueueFragment().show(parentFragmentManager, "TAG_QUEUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make marquee scroll work
|
// Make marquee scroll work
|
||||||
binding.playbackSong.isSelected = true
|
binding.playbackSong.isSelected = true
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(this)
|
binding.playbackSeekBar.setOnSeekBarChangeListener(this)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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_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
|
||||||
|
|
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_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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue