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:
parent
3ab425839c
commit
b3156941d4
11 changed files with 242 additions and 75 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -204,6 +204,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play all songs
|
||||
*/
|
||||
fun playAll() {
|
||||
playbackManager.playAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle all songs
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>
|
29
app/src/main/res/layout/item_play_shuffle.xml
Normal file
29
app/src/main/res/layout/item_play_shuffle.xml
Normal 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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue