Move user queue clear action into header

Move the button for clearing the user queue into the header for the user queue, so that its more consistent.
This commit is contained in:
OxygenCobalt 2020-11-23 10:50:45 -07:00
parent 1d50d24c4f
commit 844870f4d4
17 changed files with 165 additions and 65 deletions

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs"

View file

@ -11,6 +11,7 @@ import androidx.navigation.fragment.findNavController
* A Base [Fragment] implementing a [OnBackPressedCallback] so that Auxio will navigate upwards
* instead of out of the app if a Detail Fragment is currently open. Also carries the
* multi-navigation fix.
* // TODO: Merge headers with recyclerview [if possible]
* @author OxygenCobalt
*/
abstract class DetailFragment : Fragment() {

View file

@ -12,20 +12,20 @@ import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
class DetailAlbumAdapter(
private val doOnClick: (data: Album) -> Unit,
private val doOnLongClick: (data: Album, view: View) -> Unit
) : ListAdapter<Album, DetailAlbumAdapter.ViewHolder>(DiffCallback()) {
) : ListAdapter<Album, DetailAlbumAdapter.AlbumViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder {
return AlbumViewHolder(
ItemArtistAlbumBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
holder.bind(getItem(position))
}
// Generic ViewHolder for a detail album
inner class ViewHolder(
inner class AlbumViewHolder(
private val binding: ItemArtistAlbumBinding,
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick) {

View file

@ -120,5 +120,6 @@ data class Genre(
*/
data class Header(
override val id: Long = -1,
override var name: String = ""
override var name: String = "",
val isAction: Boolean = false
) : BaseModel()

View file

@ -14,6 +14,7 @@ import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.ui.createToast
/**
* A [Fragment] that displays the currently played song at a glance, with some basic controls.
@ -57,6 +58,7 @@ class CompactPlaybackFragment : Fragment() {
binding.root.setOnLongClickListener {
playbackModel.save(requireContext())
getString(R.string.debug_state_saved).createToast(requireContext())
true
}

View file

@ -105,17 +105,28 @@ fun NotificationCompat.Builder.setMetadata(song: Song, context: Context, onDone:
}
}
// I have no idea how to update a specific action on the fly so I have to use these restricted APIs
/**
* Update the playing button on the media notification.
* @param context The context required to refresh the action
*/
@SuppressLint("RestrictedApi")
fun NotificationCompat.Builder.updatePlaying(context: Context) {
mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
}
/**
* Update the loop button on the media notification
* @param context The context required to refresh the action
*/
@SuppressLint("RestrictedApi")
fun NotificationCompat.Builder.updateLoop(context: Context) {
mActions[0] = newAction(NotificationUtils.ACTION_LOOP, context)
}
/**
* Update the subtext of the media notification to reflect the current mode.
* @param context The context required to get the strings required to show certain modes
*/
fun NotificationCompat.Builder.updateMode(context: Context) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
val playbackManager = PlaybackStateManager.getInstance()
@ -164,12 +175,10 @@ private fun newAction(action: String, context: Context): NotificationCompat.Acti
}
return NotificationCompat.Action.Builder(
drawable, action, newPlaybackIntent(action, context)
drawable, action,
PendingIntent.getBroadcast(
context, NotificationUtils.REQUEST_CODE,
Intent(action), PendingIntent.FLAG_UPDATE_CURRENT
)
).build()
}
private fun newPlaybackIntent(action: String, context: Context): PendingIntent {
return PendingIntent.getBroadcast(
context, NotificationUtils.REQUEST_CODE, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT
)
}

View file

@ -263,8 +263,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
}
}
// TODO: Do testing where service is destroyed after restore [Possible edge case]
override fun onLoopUpdate(mode: LoopMode) {
changeIsFromAudioFocus = false
@ -296,6 +294,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
player.prepare()
player.seekTo(playbackManager.position)
}
when (playbackManager.loopMode) {
LoopMode.NONE -> {
player.repeatMode = Player.REPEAT_MODE_OFF
}
else -> {
player.repeatMode = Player.REPEAT_MODE_ONE
}
}
}
private fun restoreNotification() {

View file

@ -8,6 +8,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
@ -25,7 +27,8 @@ import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
* @author OxygenCobalt
*/
class QueueAdapter(
private val touchHelper: ItemTouchHelper
private val touchHelper: ItemTouchHelper,
private val onHeaderAction: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data = mutableListOf<BaseModel>()
private var listDiffer = AsyncListDiffer(this, DiffCallback())
@ -36,7 +39,10 @@ class QueueAdapter(
val item = data[position]
return if (item is Header)
HeaderViewHolder.ITEM_TYPE
if (item.isAction)
USER_QUEUE_HEADER_ITEM_tYPE
else
HeaderViewHolder.ITEM_TYPE
else
QUEUE_ITEM_TYPE
}
@ -47,6 +53,9 @@ class QueueAdapter(
QUEUE_ITEM_TYPE -> QueueSongViewHolder(
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
)
USER_QUEUE_HEADER_ITEM_tYPE -> UserQueueHeaderViewHolder(
ItemActionHeaderBinding.inflate(LayoutInflater.from(parent.context))
)
else -> error("Someone messed with the ViewHolder item types.")
}
}
@ -54,7 +63,12 @@ class QueueAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = data[position]) {
is Song -> (holder as QueueSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
is Header ->
if (item.isAction) {
(holder as UserQueueHeaderViewHolder).bind(item)
} else {
(holder as HeaderViewHolder).bind(item)
}
else -> {
Log.e(this::class.simpleName, "Bad data fed to QueueAdapter.")
@ -131,7 +145,24 @@ class QueueAdapter(
}
}
inner class UserQueueHeaderViewHolder(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<Header>(binding, null, null) {
override fun onBind(data: Header) {
binding.header = data
binding.headerButton.apply {
setImageResource(R.drawable.ic_clear)
setOnClickListener {
clearUserQueue()
onHeaderAction()
}
}
}
}
companion object {
const val QUEUE_ITEM_TYPE = 0xA015
const val USER_QUEUE_HEADER_ITEM_tYPE = 0xA016
}
}

View file

@ -34,25 +34,16 @@ class QueueFragment : Fragment() {
val callback = QueueDragCallback(playbackModel)
val helper = ItemTouchHelper(callback)
val queueAdapter = QueueAdapter(helper)
callback.addQueueAdapter(queueAdapter)
val queueAdapter = QueueAdapter(helper) {
playbackModel.clearUserQueue()
}
val queueClearItem = binding.queueToolbar.menu.findItem(R.id.action_clear_user_queue)
callback.addQueueAdapter(queueAdapter)
// --- UI SETUP ---
binding.queueToolbar.apply {
setNavigationOnClickListener {
findNavController().navigateUp()
}
setOnMenuItemClickListener {
if (it.itemId == R.id.action_clear_user_queue) {
queueAdapter.clearUserQueue()
playbackModel.clearUserQueue()
true
} else false
}
binding.queueToolbar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.queueRecycler.apply {
@ -65,14 +56,10 @@ class QueueFragment : Fragment() {
// --- VIEWMODEL SETUP ---
playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
queueClearItem.isEnabled = false
if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) {
findNavController().navigateUp()
if (playbackModel.nextItemsInQueue.value!!.isEmpty()) {
findNavController().navigateUp()
return@observe
}
return@observe
}
queueAdapter.submitList(createQueueData())
@ -94,7 +81,7 @@ class QueueFragment : Fragment() {
if (playbackModel.userQueue.value!!.isNotEmpty()) {
queue.add(
Header(name = getString(R.string.label_next_user_queue))
Header(name = getString(R.string.label_next_user_queue), isAction = true)
)
queue.addAll(playbackModel.userQueue.value!!)
}
@ -108,7 +95,8 @@ class QueueFragment : Fragment() {
getString(R.string.label_all_songs)
else
playbackModel.parent.value!!.name
)
),
isAction = false
)
)
queue.addAll(playbackModel.nextItemsInQueue.value!!)

View file

@ -3,6 +3,8 @@ package org.oxycblt.auxio.recycler.viewholders
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.DrawableRes
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding
@ -129,7 +131,7 @@ class SongViewHolder private constructor(
}
}
open class HeaderViewHolder(
class HeaderViewHolder(
private val binding: ItemHeaderBinding
) : BaseViewHolder<Header>(binding, null, null) {
@ -147,3 +149,20 @@ open class HeaderViewHolder(
}
}
}
abstract class ActionHeaderViewHolder(
protected val binding: ItemActionHeaderBinding,
@DrawableRes private val iconRes: Int
) : BaseViewHolder<Header>(binding, null, null) {
override fun onBind(data: Header) {
binding.header = data
binding.headerButton.apply {
setImageResource(iconRes)
setOnClickListener {
onActionClick()
}
}
}
abstract fun onActionClick()
}

View file

@ -9,6 +9,7 @@ import android.view.MenuItem
import android.widget.ImageButton
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
@ -63,7 +64,6 @@ fun RecyclerView.applyDivider() {
}
fun PopupMenu.setupSongActions(song: Song, context: Context, playbackModel: PlaybackViewModel) {
inflate(R.menu.menu_song_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
@ -85,7 +85,7 @@ fun PopupMenu.setupSongActions(song: Song, context: Context, playbackModel: Play
else -> false
}
}
show()
inflateAndShow(R.menu.menu_song_actions)
}
fun PopupMenu.setupAlbumSongActions(
@ -94,7 +94,6 @@ fun PopupMenu.setupAlbumSongActions(
detailViewModel: DetailViewModel,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_album_song_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
@ -117,7 +116,7 @@ fun PopupMenu.setupAlbumSongActions(
else -> false
}
}
show()
inflateAndShow(R.menu.menu_album_song_actions)
}
fun PopupMenu.setupAlbumActions(
@ -125,7 +124,6 @@ fun PopupMenu.setupAlbumActions(
context: Context,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_album_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
@ -148,7 +146,7 @@ fun PopupMenu.setupAlbumActions(
else -> false
}
}
show()
inflateAndShow(R.menu.menu_album_actions)
}
fun PopupMenu.setupArtistActions(
@ -156,7 +154,6 @@ fun PopupMenu.setupArtistActions(
context: Context,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_detail)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
@ -179,7 +176,7 @@ fun PopupMenu.setupArtistActions(
else -> false
}
}
show()
inflateAndShow(R.menu.menu_detail)
}
fun PopupMenu.setupGenreActions(
@ -187,7 +184,6 @@ fun PopupMenu.setupGenreActions(
context: Context,
playbackModel: PlaybackViewModel
) {
inflate(R.menu.menu_detail)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
@ -210,5 +206,10 @@ fun PopupMenu.setupGenreActions(
else -> false
}
}
inflateAndShow(R.menu.menu_detail)
}
private fun PopupMenu.inflateAndShow(@MenuRes menuRes: Int) {
inflate(menuRes)
show()
}

View file

@ -21,7 +21,6 @@
android:focusable="true"
android:elevation="@dimen/elevation_normal"
app:popupTheme="@style/Widget.CustomPopup"
app:menu="@menu/menu_queue"
app:navigationIcon="@drawable/ic_down"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.HeaderViewHolder">
<data>
<variable
name="header"
type="org.oxycblt.auxio.music.Header" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/header_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:text="@{header.name}"
android:textColor="?android:attr/textColorPrimary"
android:textSize="19sp"
app:layout_constraintEnd_toStartOf="@+id/header_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Songs" />
<ImageButton
android:id="@+id/header_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/ui_header_dividers"
android:tint="?android:attr/colorPrimary"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_clear"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -2,7 +2,7 @@
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.DetailAlbumAdapter.ViewHolder">
tools:context=".detail.adapters.DetailAlbumAdapter.AlbumViewHolder">
<data>

View file

@ -1,8 +0,0 @@
<?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_clear_user_queue"
android:title="@string/label_clear_user_queue"
app:showAsAction="never" />
</menu>

View file

@ -29,6 +29,9 @@
<string name="label_channel">Music Playback</string>
<string name="label_service_playback">The music playback service for Auxio.</string>
<!-- Debug Namespace | Debug labels -->
<string name="debug_state_saved">State saved</string>
<!-- Error Namespace | Error Labels -->
<string name="error_no_music">No music found.</string>
<string name="error_music_load_failed">Music loading failed.</string>

View file

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.10"
ext.kotlin_version = "1.4.20"
repositories {
google()