home: add play/shuffle to song list

Re-add the play/shuffle options to the song list, now as a header. This
seems to be the best option UX-wise, but the implementation is really
I think the best option regarding that is to extend this idiom to all
lists or split these fragments up. Both are reasonable.
This commit is contained in:
OxygenCobalt 2021-09-26 19:45:58 -06:00
parent 3ab425839c
commit b3156941d4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 242 additions and 75 deletions

View file

@ -164,15 +164,11 @@ class HomeFragment : Fragment() {
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, tab)
R.id.home_song_list
}
DisplayMode.SHOW_ALBUMS -> {
updateSortMenu(sortItem, tab) { id ->
id != R.id.option_sort_album
}
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
R.id.home_album_list
}

View file

@ -30,10 +30,14 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.recycler.HomeAdapter
import org.oxycblt.auxio.home.recycler.ParentAdapter
import org.oxycblt.auxio.home.recycler.SongsAdapter
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode
@ -56,53 +60,34 @@ class HomeListFragment : Fragment() {
): View {
val binding = FragmentHomeListBinding.inflate(inflater)
val homeAdapter = HomeAdapter(
doOnClick = { item ->
when (item) {
is Song -> playbackModel.playSong(item)
is Album -> findNavController().navigate(
HomeFragmentDirections.actionShowAlbum(item.id)
)
is Artist -> findNavController().navigate(
HomeFragmentDirections.actionShowArtist(item.id)
)
is Genre -> findNavController().navigate(
HomeFragmentDirections.actionShowGenre(item.id)
)
else -> {
}
}
},
::newMenu
)
// Get some tab-specific values before we go ahead. More specifically, the data to use
// and the unique ID that HomeFragment's AppBarLayout uses to determine lift state.
val pos = requireNotNull(arguments).getInt(ARG_POS)
@IdRes val customId: Int
val homeAdapter: HomeAdapter<out BaseModel>
val homeData: LiveData<out List<BaseModel>>
when (homeModel.tabs.value!![pos]) {
DisplayMode.SHOW_SONGS -> {
customId = R.id.home_song_list
homeData = homeModel.songs
homeAdapter = SongsAdapter(::onSongClick, ::newMenu, playbackModel)
}
DisplayMode.SHOW_ALBUMS -> {
customId = R.id.home_album_list
homeData = homeModel.albums
homeAdapter = ParentAdapter(::onParentClick, ::newMenu)
}
DisplayMode.SHOW_ARTISTS -> {
customId = R.id.home_artist_list
homeData = homeModel.artists
homeAdapter = ParentAdapter(::onParentClick, ::newMenu)
}
DisplayMode.SHOW_GENRES -> {
customId = R.id.home_genre_list
homeData = homeModel.genres
homeAdapter = ParentAdapter(::onParentClick, ::newMenu)
}
}
@ -129,6 +114,26 @@ class HomeListFragment : Fragment() {
return binding.root
}
private fun onSongClick(song: Song) {
playbackModel.playSong(song)
}
private fun onParentClick(parent: Parent) {
when (parent) {
is Album -> findNavController().navigate(
HomeFragmentDirections.actionShowAlbum(parent.id)
)
is Artist -> findNavController().navigate(
HomeFragmentDirections.actionShowArtist(parent.id)
)
is Genre -> findNavController().navigate(
HomeFragmentDirections.actionShowGenre(parent.id)
)
}
}
companion object {
private const val ARG_POS = BuildConfig.APPLICATION_ID + ".key.POS"

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2021 Auxio Project
* HomeAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home.recycler
import android.annotation.SuppressLint
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.BaseModel
/**
* A base class that implements an [updateData] that is required across [SongsAdapter] and [ParentAdapter]
*/
abstract class HomeAdapter<T : BaseModel> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
protected var data = listOf<BaseModel>()
/**
* Update the data with [newData]. [notifyDataSetChanged] will be called.
*/
@SuppressLint("NotifyDataSetChanged")
fun updateData(newData: List<BaseModel>) {
data = newData
// I would use ListAdapter instead of this inefficient invalidate call, but they still
// haven't fixed the issue where ListAdapter's calculations will cause wild scrolling
// for basically no reason.
notifyDataSetChanged()
}
}

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2021 Auxio Project
* HomeAdapter.kt is part of Auxio.
* ParentAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,32 +16,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home
package org.oxycblt.auxio.home.recycler
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.ui.AlbumViewHolder
import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.SongViewHolder
/**
* A universal adapter for displaying data in [HomeFragment].
* A universal adapter for displaying [Parent] data.
*/
class HomeAdapter(
private val doOnClick: (data: BaseModel) -> Unit,
private val doOnLongClick: (view: View, data: BaseModel) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data = listOf<BaseModel>()
class ParentAdapter(
private val doOnClick: (data: Parent) -> Unit,
private val doOnLongClick: (view: View, data: Parent) -> Unit,
) : HomeAdapter<Parent>() {
override fun getItemCount(): Int = data.size
override fun getItemViewType(position: Int): Int {
@ -49,7 +43,6 @@ class HomeAdapter(
is Genre -> GenreViewHolder.ITEM_TYPE
is Artist -> ArtistViewHolder.ITEM_TYPE
is Album -> AlbumViewHolder.ITEM_TYPE
is Song -> SongViewHolder.ITEM_TYPE
else -> error("Unsupported item ${data[position]::class.simpleName}")
}
}
@ -68,10 +61,6 @@ class HomeAdapter(
parent.context, doOnClick, doOnLongClick
)
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
else -> error("Invalid viewholder item type.")
}
}
@ -81,20 +70,6 @@ class HomeAdapter(
is Genre -> (holder as GenreViewHolder).bind(item)
is Artist -> (holder as ArtistViewHolder).bind(item)
is Album -> (holder as AlbumViewHolder).bind(item)
is Song -> (holder as SongViewHolder).bind(item)
}
}
/**
* Update the data with [newData]. [notifyDataSetChanged] will be called.
*/
@SuppressLint("NotifyDataSetChanged")
fun updateData(newData: List<BaseModel>) {
data = newData
// I would use ListAdapter instead of this inefficient invalidate call, but they still
// haven't fixed the issue where ListAdapter's calculations will cause wild scrolling
// for basically no reason.
notifyDataSetChanged()
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2021 Auxio Project
* HomeAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home.recycler
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPlayShuffleBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.util.inflater
/**
* An adapter for displaying a song list with a special play/shuffle header.
* Note that the data for the play/pause icon does not need to be included with the data
* you are submitting. It is automatically handled by the adapter.
* TODO: Maybe extend play/shuffle to all items?
*/
class SongsAdapter(
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (view: View, data: Song) -> Unit,
private val playbackModel: PlaybackViewModel
) : HomeAdapter<Song>() {
override fun getItemCount(): Int = if
(data.isEmpty()) 0 // Account for the play/shuffle viewholder
else
data.size + 1
override fun getItemViewType(position: Int): Int {
return if (position == 0) {
PLAY_ITEM_TYPE
} else {
SongViewHolder.ITEM_TYPE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
PLAY_ITEM_TYPE -> PlayViewHolder(
ItemPlayShuffleBinding.inflate(parent.context.inflater)
)
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
parent.context, doOnClick, doOnLongClick
)
else -> error("Invalid viewholder item type.")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is SongViewHolder) {
holder.bind(data[position - 1] as Song)
}
}
private inner class PlayViewHolder(
binding: ItemPlayShuffleBinding
) : RecyclerView.ViewHolder(binding.root) {
init {
// Force the layout to *actually* be the screen width.
// We can't inherit BaseViewHolder here since this ViewHolder isn't really connected
// to an item.
binding.root.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
)
binding.playButton.setOnClickListener {
playbackModel.playAll()
}
binding.shuffleButton.setOnClickListener {
playbackModel.shuffleAll()
}
}
}
companion object {
const val PLAY_ITEM_TYPE = 0xA00E
}
}

View file

@ -40,13 +40,14 @@ import org.oxycblt.auxio.util.logD
*
* You think that if you wanted to query a song's genre from a media database, you could just
* put "genre" in the query and it would return it, right? But not with MediaStore! No, that's
* to straightfoward for this platform. So instead, you have to query for each genre, query all
* the songs in each genre, and then iterate through those songs to link every song with their
* genre. This is not documented anywhere in MediaStore's documentation, and the O(mom im scared)
* algorithm you have to run to get it working single-handedly DOUBLES Auxio's loading times. At
* no point have the devs considered that this column is absolutely busted, and instead focused on
* adding infuriat- I mean nice proprietary extensions to MediaStore for their own Google Play Music,
* and we all know how great that worked out!
* to straightfoward for this platform that was dropped on it's head as a baby. So instead, you
* have to query for each genre, query all the songs in each genre, and then iterate through those
* songs to link every song with their genre. This is not documented anywhere in MediaStore's
* documentation, and the O(mom im scared) algorithm you have to run to get it working
* single-handedly DOUBLES Auxio's loading times. At no point have the devs considered that this
* column is absolutely busted, and instead focused on adding infuriat- I mean nice proprietary
* extensions to MediaStore for their own Google Play Music, and we all know how great that worked
* out!
*
* It's not even ergonomics that makes this API bad. It's base implementation is completely borked
* as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files?
@ -55,21 +56,21 @@ import org.oxycblt.auxio.util.logD
* DATE tag. Once again, this is because internally android uses an ancient in-house metadata
* parser to get everything indexed, and so far they have not bothered to modernize this parser
* or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 is
* 21 years old. It can drink now. All my what.
* 21 years old. It can drink now. All of my what.
*
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
* table, so we have to go for the less efficent "make a big query on all the songs lol" method
* so that songs don't end up fragmented across artists. Pretty much every OEM has added some
* extension or quirk to MediaStore that I cannot determine, with some OEMs (COUGHSAMSUNGCOUGH)
* extension or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH)
* crippling the normal tables so that you're railroaded into their ad-infested music app.
* The way I do blacklisting relies on a deprecated method, and the supposedly "modern" method
* is SLOWER and causes even more problems since I have to manage databases across version
* boundaries. Sometimes music will have a deformed clone that I can't filter out, sometimes
* Genre's will just break for no reason, sometimes this plate of spaghetti just completely breaks
* Genres will just break for no reason, sometimes this spaghetti parser just completely breaks
* down and is unable to get any metadata. Everything is broken in it's own special unique way and
* I absolutely hate it.
*
* Is there anything we can do about it? No. Google has routinely shut down issues that beg google
* Is there anything we can do about it? No. Google has routinely shut down issues that begged google
* to fix glaring issues with MediaStore or to just take the API behind the woodshed and shoot it.
* Largely because they have zero incentive to improve it, especially for such obscure things
* as indexing music. As a result, some players like Vanilla and VLC just hack their own pidgin
@ -84,7 +85,7 @@ import org.oxycblt.auxio.util.logD
* I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and
* probably deprecated eventually for a "new" API that just coincidentally excludes music indexing.
* Because go **** yourself for wanting to listen to music you own. Be a good consoomer and listen
* to your AlgoMix MusikStream.
* to your AlgoPop StreamMix.
*
* I hate this platform so much.
*

View file

@ -204,6 +204,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
}
/**
* Play all songs
*/
fun playAll() {
playbackManager.playAll()
}
/**
* Shuffle all songs
*/

View file

@ -232,6 +232,18 @@ class PlaybackStateManager private constructor() {
updatePlayback(mQueue[0])
}
/**
* Play all songs.
*/
fun playAll() {
mMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList()
mParent = null
setShuffling(false, keepSong = false)
updatePlayback(mQueue[0])
}
/**
* Shuffle all songs.
*/

View file

@ -49,6 +49,6 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_home_list" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="@dimen/spacing_medium"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/play_button"
style="@style/Widget.Auxio.Button.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@string/lbl_play" />
<com.google.android.material.button.MaterialButton
android:id="@+id/shuffle_button"
style="@style/Widget.Auxio.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="@dimen/spacing_small"
android:text="@string/lbl_shuffle" />
</LinearLayout>
</layout>

View file

@ -56,6 +56,7 @@ To prevent any strange bugs, all integer representations must be unique. A table
0xA00C | GenreSongViewHolder
0xA00D | QueueSongViewHolder
0xA00E | PlayPauseViewHolder
0xA0A0 | Auxio notification code
0xA0C0 | Auxio request code