home: re-add sorting

Re-add sorting to HomeFragment, except heavily improved. The major
improvement here is the addition of song sorting, which was a heavily
requested feature judging by #16. The setting does not save yet and
is not present in the detail fragments, but it is still a major
milestone for the new home ui.
This commit is contained in:
OxygenCobalt 2021-09-05 16:11:37 -06:00
parent 0fc8f1cd02
commit dae334b1d6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
39 changed files with 371 additions and 143 deletions

View file

@ -32,6 +32,9 @@ import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.GenreViewHolder import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.SongViewHolder
/**
* A universal adapter for displaying data in [HomeFragment].
*/
class HomeAdapter( class HomeAdapter(
private val doOnClick: (data: BaseModel) -> Unit, private val doOnClick: (data: BaseModel) -> Unit,
private val doOnLongClick: (view: View, data: BaseModel) -> Unit private val doOnLongClick: (view: View, data: BaseModel) -> Unit
@ -89,6 +92,9 @@ class HomeAdapter(
fun updateData(newData: List<BaseModel>) { fun updateData(newData: List<BaseModel>) {
data = newData 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() notifyDataSetChanged()
} }
} }

View file

@ -20,8 +20,10 @@ package org.oxycblt.auxio.home
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.iterator
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -48,13 +50,9 @@ import org.oxycblt.auxio.util.makeScrollingViewFade
/** /**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail * The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective fragment. * views for each respective fragment.
* TODO: Re-add sorting (but new and improved)
* It will require a new SortMode to be made simply for compat. Migrate the old SortMode
* eventually.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
@ -64,6 +62,7 @@ class HomeFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeBinding.inflate(inflater) val binding = FragmentHomeBinding.inflate(inflater)
val sortItem: MenuItem
// --- UI SETUP --- // --- UI SETUP ---
@ -75,20 +74,35 @@ class HomeFragment : Fragment() {
binding.homeAppbar.makeScrollingViewFade(binding.homeToolbar) binding.homeAppbar.makeScrollingViewFade(binding.homeToolbar)
binding.homeToolbar.setOnMenuItemClickListener { item -> binding.homeToolbar.apply {
when (item.itemId) { setOnMenuItemClickListener { item ->
R.id.action_settings -> { when (item.itemId) {
parentFragment?.parentFragment?.findNavController()?.navigate( R.id.action_settings -> {
MainFragmentDirections.actionShowSettings() parentFragment?.parentFragment?.findNavController()?.navigate(
) MainFragmentDirections.actionShowSettings()
)
}
R.id.action_search -> {
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
}
R.id.submenu_sorting -> { }
// Sorting option was selected, check then and update the mode
else -> {
item.isChecked = true
homeModel.updateCurrentSort(
requireNotNull(LibSortMode.fromId(item.itemId))
)
}
} }
R.id.action_search -> { true
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
}
} }
true sortItem = menu.findItem(R.id.submenu_sorting)
} }
binding.homePager.apply { binding.homePager.apply {
@ -121,24 +135,13 @@ class HomeFragment : Fragment() {
// page transitions. // page transitions.
offscreenPageLimit = homeModel.tabs.value!!.size offscreenPageLimit = homeModel.tabs.value!!.size
// ViewPager2 tends to garble any scrolling view events that occur within it's
// fragments, so we fix that by instructing our AppBarLayout to follow the specific
// view we have just selected.
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
binding.homeAppbar.liftOnScrollTargetViewId =
when (homeModel.tabs.value!![position]) {
DisplayMode.SHOW_SONGS -> R.id.home_song_list
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list
DisplayMode.SHOW_GENRES -> R.id.home_genre_list
}
}
}) })
} }
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos -> TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
val labelRes = when (requireNotNull(homeModel.tabs.value)[pos]) { val labelRes = when (homeModel.tabs.value!![pos]) {
DisplayMode.SHOW_SONGS -> R.string.lbl_songs DisplayMode.SHOW_SONGS -> R.string.lbl_songs
DisplayMode.SHOW_ALBUMS -> R.string.lbl_albums DisplayMode.SHOW_ALBUMS -> R.string.lbl_albums
DisplayMode.SHOW_ARTISTS -> R.string.lbl_artists DisplayMode.SHOW_ARTISTS -> R.string.lbl_artists
@ -150,6 +153,40 @@ class HomeFragment : Fragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
binding.homeAppbar.liftOnScrollTargetViewId = when (requireNotNull(tab)) {
DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, homeModel.songSortMode)
R.id.home_song_list
}
DisplayMode.SHOW_ALBUMS -> {
updateSortMenu(sortItem, homeModel.albumSortMode) { id ->
id != R.id.option_sort_album
}
R.id.home_album_list
}
DisplayMode.SHOW_ARTISTS -> {
updateSortMenu(sortItem, homeModel.artistSortMode) { id ->
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
}
R.id.home_artist_list
}
DisplayMode.SHOW_GENRES -> {
updateSortMenu(sortItem, homeModel.genreSortMode) { id ->
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
}
R.id.home_genre_list
}
}
}
detailModel.navToItem.observe(viewLifecycleOwner) { item -> detailModel.navToItem.observe(viewLifecycleOwner) { item ->
// The AppBarLayout gets confused and collapses when we navigate too fast, wait for it // The AppBarLayout gets confused and collapses when we navigate too fast, wait for it
// to draw before we continue. // to draw before we continue.
@ -182,10 +219,24 @@ class HomeFragment : Fragment() {
return binding.root return binding.root
} }
private fun updateSortMenu(
item: MenuItem,
toHighlight: LibSortMode,
isVisible: (Int) -> Boolean = { true }
) {
for (option in item.subMenu) {
if (option.itemId == toHighlight.itemId) {
option.isChecked = true
}
option.isVisible = isVisible(option.itemId)
}
}
private inner class HomePagerAdapter : private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
override fun getItemCount(): Int = requireNotNull(homeModel.tabs.value).size override fun getItemCount(): Int = homeModel.tabs.value!!.size
override fun createFragment(position: Int): Fragment = HomeListFragment.new(position) override fun createFragment(position: Int): Fragment = HomeListFragment.new(position)
} }
} }

View file

@ -24,7 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -38,7 +38,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -47,8 +46,8 @@ import org.oxycblt.auxio.util.logD
* should be created using the [new] method with it's position in the ViewPager. * should be created using the [new] method with it's position in the ViewPager.
*/ */
class HomeListFragment : Fragment() { class HomeListFragment : Fragment() {
private val homeModel: HomeViewModel by viewModels() private val homeModel: HomeViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by viewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -81,36 +80,36 @@ class HomeListFragment : Fragment() {
::newMenu ::newMenu
) )
// --- ITEM SETUP ---
// Get some tab-specific values before we go ahead. More specifically, the data to use // 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. // and the unique ID that HomeFragment's AppBarLayout uses to determine lift state.
val pos = requireNotNull(arguments).getInt(ARG_POS) val pos = requireNotNull(arguments).getInt(ARG_POS)
@IdRes val customId: Int @IdRes val customId: Int
val toObserve: LiveData<out List<BaseModel>> val homeData: LiveData<out List<BaseModel>>
when (requireNotNull(homeModel.tabs.value)[pos]) { when (homeModel.tabs.value!![pos]) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
customId = R.id.home_song_list customId = R.id.home_song_list
toObserve = homeModel.songs homeData = homeModel.songs
} }
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
customId = R.id.home_album_list customId = R.id.home_album_list
toObserve = homeModel.albums homeData = homeModel.albums
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
customId = R.id.home_artist_list customId = R.id.home_artist_list
toObserve = homeModel.artists homeData = homeModel.artists
} }
DisplayMode.SHOW_GENRES -> { DisplayMode.SHOW_GENRES -> {
customId = R.id.home_genre_list customId = R.id.home_genre_list
toObserve = homeModel.genres homeData = homeModel.genres
} }
} }
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = customId id = customId
adapter = homeAdapter adapter = homeAdapter
@ -121,16 +120,8 @@ class HomeListFragment : Fragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
// Make sure that this RecyclerView has data before startup // Make sure that this RecyclerView has data before startup
homeAdapter.updateData(toObserve.value!!) homeData.observe(viewLifecycleOwner) { data ->
homeAdapter.updateData(data)
toObserve.observe(viewLifecycleOwner) { data ->
homeAdapter.updateData(
data.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) {
it.name.sliceArticle()
}
)
)
} }
logD("Fragment created") logD("Fragment created")

View file

@ -28,32 +28,85 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
/**
* The ViewModel for managing [HomeFragment]'s data and sorting modes.
*/
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
private val mGenres = MutableLiveData(listOf<Genre>()) private val mSongs = MutableLiveData(listOf<Song>())
val genres: LiveData<List<Genre>> get() = mGenres val songs: LiveData<List<Song>> get() = mSongs
private val mArtists = MutableLiveData(listOf<Artist>())
val artists: LiveData<List<Artist>> get() = mArtists
private val mAlbums = MutableLiveData(listOf<Album>()) private val mAlbums = MutableLiveData(listOf<Album>())
val albums: LiveData<List<Album>> get() = mAlbums val albums: LiveData<List<Album>> get() = mAlbums
private val mSongs = MutableLiveData(listOf<Song>()) private val mArtists = MutableLiveData(listOf<Artist>())
val songs: LiveData<List<Song>> get() = mSongs val artists: LiveData<List<Artist>> get() = mArtists
private val mTabs = MutableLiveData(arrayOf<DisplayMode>()) private val mGenres = MutableLiveData(listOf<Genre>())
val genres: LiveData<List<Genre>> get() = mGenres
private val mTabs = MutableLiveData(
arrayOf(
DisplayMode.SHOW_SONGS, DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_GENRES
)
)
val tabs: LiveData<Array<DisplayMode>> = mTabs val tabs: LiveData<Array<DisplayMode>> = mTabs
private val mCurTab = MutableLiveData(mTabs.value!![0])
val curTab: LiveData<DisplayMode> = mCurTab
var genreSortMode = LibSortMode.ASCENDING
private set
var artistSortMode = LibSortMode.ASCENDING
private set
var albumSortMode = LibSortMode.ASCENDING
private set
var songSortMode = LibSortMode.ASCENDING
private set
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
init { init {
mGenres.value = musicStore.genres mSongs.value = songSortMode.sortSongs(musicStore.songs)
mArtists.value = musicStore.artists mAlbums.value = albumSortMode.sortAlbums(musicStore.albums)
mAlbums.value = musicStore.albums mArtists.value = artistSortMode.sortModels(musicStore.artists)
mSongs.value = musicStore.songs mGenres.value = genreSortMode.sortModels(musicStore.genres)
mTabs.value = arrayOf( }
DisplayMode.SHOW_SONGS, DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_GENRES /**
) * Update the current tab based off of the new ViewPager position.
*/
fun updateCurrentTab(pos: Int) {
val mode = mTabs.value!![pos]
mCurTab.value = mode
}
/**
* Update the currently displayed item's [LibSortMode].
*/
fun updateCurrentSort(sort: LibSortMode) {
when (mCurTab.value) {
DisplayMode.SHOW_SONGS -> {
songSortMode = sort
mSongs.value = sort.sortSongs(mSongs.value!!)
}
DisplayMode.SHOW_ALBUMS -> {
albumSortMode = sort
mAlbums.value = sort.sortAlbums(mAlbums.value!!)
}
DisplayMode.SHOW_ARTISTS -> {
artistSortMode = sort
mArtists.value = sort.sortModels(mArtists.value!!)
}
DisplayMode.SHOW_GENRES -> {
genreSortMode = sort
mGenres.value = sort.sortModels(mGenres.value!!)
}
}
} }
} }

View file

@ -0,0 +1,148 @@
/*
* Copyright (c) 2021 Auxio Project
* LibSortMode.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
import androidx.annotation.IdRes
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.sliceArticle
/**
* The enum for the current sort state.
* This enum is semantic depending on the context it is used. Documentation describing each
* sorting functions behavior can be found in the function definition.
* @param itemId Menu ID associated with this enum
* @author OxygenCobalt
*/
enum class LibSortMode(@IdRes val itemId: Int) {
ASCENDING(R.id.option_sort_asc),
DESCENDING(R.id.option_sort_dsc),
ARTIST(R.id.option_sort_artist),
ALBUM(R.id.option_sort_album),
YEAR(R.id.option_sort_year);
/**
* Sort a list of songs.
*
* **Behavior:**
* - [ASCENDING] & [DESCENDING]: See [sortModels]
* - [ARTIST]: Grouped by album and then sorted [ASCENDING] based off the artist name.
* - [ALBUM]: Grouped by album and sorted [ASCENDING]
* - [YEAR]: Grouped by album and sorted by year
*
* The grouping mode for songs in an album will be by track, [ASCENDING].
* @see sortAlbums
*/
fun sortSongs(songs: Collection<Song>): List<Song> {
return when (this) {
ASCENDING, DESCENDING -> sortModels(songs)
else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
ASCENDING.sortAlbum(album)
}
}
}
/**
* Sort a list of albums.
*
* **Behavior:**
* - [ASCENDING] & [DESCENDING]: See [sortModels]
* - [ARTIST]: Grouped by artist and sorted [ASCENDING]
* - [ALBUM]: [ASCENDING]
* - [YEAR]: Sorted by year
*
* The grouping mode for albums in an artist will be [YEAR].
*/
fun sortAlbums(albums: Collection<Album>): List<Album> {
return when (this) {
ASCENDING, DESCENDING -> sortModels(albums)
ARTIST -> ASCENDING.sortModels(albums.groupBy { it.artist }.keys)
.flatMap { YEAR.sortAlbums(it.albums) }
ALBUM -> ASCENDING.sortModels(albums)
YEAR -> albums.sortedByDescending { it.year }
}
}
/**
* Sort a list of generic [BaseModel] instances.
*
* **Behavior:**
* - [ASCENDING]: Sorted by name, ascending
* - [DESCENDING]: Sorted by name, descending
* - Same list is returned otherwise.
*
* Names will be treated as case-insensitive. Articles like "the" and "a" will be skipped
* to line up with MediaStore behavior.
*/
fun <T : BaseModel> sortModels(models: Collection<T>): List<T> {
return when (this) {
ASCENDING -> models.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { model ->
model.name.sliceArticle()
}
)
DESCENDING -> models.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) { model ->
model.name.sliceArticle()
}
)
else -> models.toList()
}
}
/**
* Sort the songs in an album.
*
* **Behavior:**
* - [ASCENDING]: By track, ascending
* - [DESCENDING]: By track, descending
* - Same song list is returned otherwise.
*/
fun sortAlbum(album: Album): List<Song> {
return when (this) {
ASCENDING -> album.songs.sortedBy { it.track }
DESCENDING -> album.songs.sortedByDescending { it.track }
else -> album.songs
}
}
companion object {
/**
* Convert a menu [id] to an instance of [LibSortMode].
*/
fun fromId(@IdRes id: Int): LibSortMode? {
return when (id) {
ASCENDING.itemId -> ASCENDING
DESCENDING.itemId -> DESCENDING
ARTIST.itemId -> ARTIST
ALBUM.itemId -> ALBUM
YEAR.itemId -> YEAR
else -> null
}
}
}
}

View file

@ -149,7 +149,7 @@ class PlaybackNotification private constructor(
loopMode: LoopMode loopMode: LoopMode
): NotificationCompat.Action { ): NotificationCompat.Action {
val drawableRes = when (loopMode) { val drawableRes = when (loopMode) {
LoopMode.NONE -> R.drawable.ic_loop_inactive LoopMode.NONE -> R.drawable.ic_loop_off
LoopMode.ALL -> R.drawable.ic_loop LoopMode.ALL -> R.drawable.ic_loop
LoopMode.TRACK -> R.drawable.ic_loop_one LoopMode.TRACK -> R.drawable.ic_loop_one
} }
@ -161,7 +161,7 @@ class PlaybackNotification private constructor(
context: Context, context: Context,
isShuffled: Boolean isShuffled: Boolean
): NotificationCompat.Action { ): NotificationCompat.Action {
val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_shuffle_inactive val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_shuffle_off
return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes) return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes)
} }

View file

@ -29,8 +29,7 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
/** /**
* An enum for the current sorting mode. Contains helper functions to sort lists based * The legacy enum for sorting. This is set to be removed soon.
* off the given sorting mode.
* @property iconRes The icon for this [SortMode] * @property iconRes The icon for this [SortMode]
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@ -190,10 +189,9 @@ enum class SortMode(@DrawableRes val iconRes: Int) {
@IdRes @IdRes
fun toMenuId(): Int { fun toMenuId(): Int {
return when (this) { return when (this) {
ALPHA_UP -> R.id.option_sort_alpha_up ALPHA_UP -> R.id.option_sort_asc
ALPHA_DOWN -> R.id.option_sort_alpha_down ALPHA_DOWN -> R.id.option_sort_dsc
else -> R.id.option_sort_dsc
else -> R.id.option_sort_alpha_up
} }
} }

View file

@ -27,11 +27,9 @@ import android.util.TypedValue
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -39,9 +37,6 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
// TODO: Make a helper AppBarLayout of some kind that auto-updates the lifted state. I know
// what to do, it's just hard to make it work correctly.
/** /**
* Apply the recommended spans for a [RecyclerView]. * Apply the recommended spans for a [RecyclerView].
* *
@ -72,6 +67,7 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
/** /**
* Disable an image button. * Disable an image button.
* TODO: Replace this fragile function with something else.
*/ */
fun ImageButton.disable() { fun ImageButton.disable() {
if (isEnabled) { if (isEnabled) {
@ -79,14 +75,6 @@ fun ImageButton.disable() {
isEnabled = false isEnabled = false
} }
} }
/**
* Set a [TextView] text color, without having to resolve the resource.
*/
fun TextView.setTextColorResource(@ColorRes color: Int) {
setTextColor(color.resolveColor(context))
}
/** /**
* Returns whether a recyclerview can scroll. * Returns whether a recyclerview can scroll.
*/ */
@ -116,20 +104,14 @@ fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
* @see resolveColor * @see resolveColor
*/ */
fun @receiver:ColorRes Int.resolveStateList(context: Context) = fun @receiver:ColorRes Int.resolveStateList(context: Context) =
ColorStateList.valueOf(resolveColor(context)) ContextCompat.getColorStateList(context, this)
/**
* Resolve a drawable resource into a [Drawable]
*/
fun @receiver:DrawableRes Int.resolveDrawable(context: Context) =
requireNotNull(ContextCompat.getDrawable(context, this))
/** /**
* Resolve this int into a color as if it was an attribute * Resolve this int into a color as if it was an attribute
*/ */
@ColorInt @ColorInt
fun @receiver:AttrRes Int.resolveAttr(context: Context): Int { fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
// Convert the attribute into its color // First resolve the attribute into its ID
val resolvedAttr = TypedValue() val resolvedAttr = TypedValue()
context.theme.resolveAttribute(this, resolvedAttr, true) context.theme.resolveAttribute(this, resolvedAttr, true)

View file

@ -123,7 +123,7 @@ fun createFullWidget(context: Context, state: WidgetState): RemoteViews {
// And no, we can't control state drawables with RemoteViews. Because of course we can't. // And no, we can't control state drawables with RemoteViews. Because of course we can't.
val shuffleRes = when { val shuffleRes = when {
state.isShuffled -> R.drawable.ic_shuffle_tinted state.isShuffled -> R.drawable.ic_shuffle_on
else -> R.drawable.ic_shuffle else -> R.drawable.ic_shuffle
} }

View file

@ -63,7 +63,7 @@
<ImageButton <ImageButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Button.Unbounded" style="@style/Widget.Button.Unbounded"
android:src="@drawable/ic_playing_state" android:src="@drawable/sel_playing_state"
android:layout_margin="@dimen/spacing_small" android:layout_margin="@dimen/spacing_small"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:onClick="@{() -> playbackModel.invertPlayingStatus()}" android:onClick="@{() -> playbackModel.invertPlayingStatus()}"

View file

@ -46,7 +46,7 @@
android:id="@+id/widget_play_pause" android:id="@+id/widget_play_pause"
style="@style/Widget.Component.AppWidget.Button" style="@style/Widget.Component.AppWidget.Button"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_playing_state" /> android:src="@drawable/sel_playing_state" />
<ImageButton <ImageButton
android:id="@+id/widget_skip_next" android:id="@+id/widget_skip_next"

View file

@ -16,11 +16,11 @@
<menu> <menu>
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
<item <item
android:id="@+id/option_sort_alpha_down" android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_alpha_down" /> android:title="@string/lbl_sort_asc" />
<item <item
android:id="@+id/option_sort_alpha_up" android:id="@+id/option_sort_dsc"
android:title="@string/lbl_sort_alpha_up" /> android:title="@string/lbl_sort_dsc" />
<item <item
android:id="@+id/option_sort_artist" android:id="@+id/option_sort_artist"
android:title="@string/lbl_sort_artist" /> android:title="@string/lbl_sort_artist" />

View file

@ -20,8 +20,8 @@
<string name="lbl_filter">"Filtr"</string> <string name="lbl_filter">"Filtr"</string>
<string name="lbl_filter_all">"Vše"</string> <string name="lbl_filter_all">"Vše"</string>
<string name="lbl_sort">"Řadit"</string> <string name="lbl_sort">"Řadit"</string>
<string name="lbl_sort_alpha_down">"Vzestupně"</string> <string name="lbl_sort_asc">"Vzestupně"</string>
<string name="lbl_sort_alpha_up">"Sestupně"</string> <string name="lbl_sort_dsc">"Sestupně"</string>
<string name="lbl_sort_artist">"Umělec"</string> <string name="lbl_sort_artist">"Umělec"</string>
<string name="lbl_sort_album">"Album"</string> <string name="lbl_sort_album">"Album"</string>
<string name="lbl_sort_year">"Rok"</string> <string name="lbl_sort_year">"Rok"</string>

View file

@ -19,8 +19,8 @@
<string name="lbl_filter_all">Alles</string> <string name="lbl_filter_all">Alles</string>
<string name="lbl_sort">Sortierung</string> <string name="lbl_sort">Sortierung</string>
<string name="lbl_sort_alpha_down">Aufsteigend</string> <string name="lbl_sort_asc">Aufsteigend</string>
<string name="lbl_sort_alpha_up">Absteigend</string> <string name="lbl_sort_dsc">Absteigend</string>
<string name="lbl_play">Abspielen</string> <string name="lbl_play">Abspielen</string>
<string name="lbl_shuffle">Zufällig</string> <string name="lbl_shuffle">Zufällig</string>

View file

@ -20,8 +20,8 @@
<string name="lbl_filter_all">Todo</string> <string name="lbl_filter_all">Todo</string>
<string name="lbl_sort">Ordenar</string> <string name="lbl_sort">Ordenar</string>
<string name="lbl_sort_alpha_down">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
<string name="lbl_sort_alpha_up">Descendente</string> <string name="lbl_sort_dsc">Descendente</string>
<string name="lbl_play">Reproducir</string> <string name="lbl_play">Reproducir</string>
<string name="lbl_shuffle">Aleatorio</string> <string name="lbl_shuffle">Aleatorio</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Tout</string> <string name="lbl_filter_all">Tout</string>
<string name="lbl_sort">Tri</string> <string name="lbl_sort">Tri</string>
<string name="lbl_sort_alpha_down">Ascendant</string> <string name="lbl_sort_asc">Ascendant</string>
<string name="lbl_sort_alpha_up">Descendant</string> <string name="lbl_sort_dsc">Descendant</string>
<string name="lbl_play">Lecture</string> <string name="lbl_play">Lecture</string>
<string name="lbl_shuffle">Aléatoire</string> <string name="lbl_shuffle">Aléatoire</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Összes</string> <string name="lbl_filter_all">Összes</string>
<string name="lbl_sort">Összes</string> <string name="lbl_sort">Összes</string>
<string name="lbl_sort_alpha_down">Növekvő</string> <string name="lbl_sort_asc">Növekvő</string>
<string name="lbl_sort_alpha_up">Csökkenő</string> <string name="lbl_sort_dsc">Csökkenő</string>
<string name="lbl_play">Lejátszás</string> <string name="lbl_play">Lejátszás</string>
<string name="lbl_shuffle">Keverés</string> <string name="lbl_shuffle">Keverés</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Semua</string> <string name="lbl_filter_all">Semua</string>
<string name="lbl_sort">Urutan</string> <string name="lbl_sort">Urutan</string>
<string name="lbl_sort_alpha_down">Naik</string> <string name="lbl_sort_asc">Naik</string>
<string name="lbl_sort_alpha_up">Turun</string> <string name="lbl_sort_dsc">Turun</string>
<string name="lbl_play">Putar</string> <string name="lbl_play">Putar</string>
<string name="lbl_shuffle">Acak</string> <string name="lbl_shuffle">Acak</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Tutto</string> <string name="lbl_filter_all">Tutto</string>
<string name="lbl_sort">Ordine</string> <string name="lbl_sort">Ordine</string>
<string name="lbl_sort_alpha_down">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
<string name="lbl_sort_alpha_up">Discendente</string> <string name="lbl_sort_dsc">Discendente</string>
<string name="lbl_play">Riproduci</string> <string name="lbl_play">Riproduci</string>
<string name="lbl_shuffle">Casuale</string> <string name="lbl_shuffle">Casuale</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">전부</string> <string name="lbl_filter_all">전부</string>
<string name="lbl_sort">분류</string> <string name="lbl_sort">분류</string>
<string name="lbl_sort_alpha_down">오름차순</string> <string name="lbl_sort_asc">오름차순</string>
<string name="lbl_sort_alpha_up">내림차순</string> <string name="lbl_sort_dsc">내림차순</string>
<string name="lbl_play">재생</string> <string name="lbl_play">재생</string>
<string name="lbl_shuffle">모든 곡 랜덤 재생</string> <string name="lbl_shuffle">모든 곡 랜덤 재생</string>

View file

@ -20,8 +20,8 @@
<string name="lbl_filter_all">Alles</string> <string name="lbl_filter_all">Alles</string>
<string name="lbl_sort">Sorteren</string> <string name="lbl_sort">Sorteren</string>
<string name="lbl_sort_alpha_down">Oplopend</string> <string name="lbl_sort_asc">Oplopend</string>
<string name="lbl_sort_alpha_up">Aflopend</string> <string name="lbl_sort_dsc">Aflopend</string>
<string name="lbl_play">Afspelen</string> <string name="lbl_play">Afspelen</string>
<string name="lbl_shuffle">Shuffle</string> <string name="lbl_shuffle">Shuffle</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Wszystkie</string> <string name="lbl_filter_all">Wszystkie</string>
<string name="lbl_sort">Sortowanie</string> <string name="lbl_sort">Sortowanie</string>
<string name="lbl_sort_alpha_down">Rosnąco</string> <string name="lbl_sort_asc">Rosnąco</string>
<string name="lbl_sort_alpha_up">Malejąco</string> <string name="lbl_sort_dsc">Malejąco</string>
<string name="lbl_play">Graj</string> <string name="lbl_play">Graj</string>
<string name="lbl_shuffle">Losowo</string> <string name="lbl_shuffle">Losowo</string>

View file

@ -16,7 +16,7 @@
<string name="lbl_filter_all">Tudo</string> <string name="lbl_filter_all">Tudo</string>
<string name="lbl_sort">Classificação</string> <string name="lbl_sort">Classificação</string>
<string name="lbl_sort_alpha_up">Descendente</string> <string name="lbl_sort_dsc">Descendente</string>
<string name="lbl_play">Reproduzir</string> <string name="lbl_play">Reproduzir</string>
<string name="lbl_shuffle">Embaralhar</string> <string name="lbl_shuffle">Embaralhar</string>
@ -98,5 +98,5 @@
<item quantity="one">%d Álbum</item> <item quantity="one">%d Álbum</item>
<item quantity="other">%d Álbuns</item> <item quantity="other">%d Álbuns</item>
</plurals> </plurals>
<string name="lbl_sort_alpha_down">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
</resources> </resources>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Tudo</string> <string name="lbl_filter_all">Tudo</string>
<string name="lbl_sort">Classificação</string> <string name="lbl_sort">Classificação</string>
<string name="lbl_sort_alpha_down">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
<string name="lbl_sort_alpha_up">Descendente</string> <string name="lbl_sort_dsc">Descendente</string>
<string name="lbl_play">Reproduzir</string> <string name="lbl_play">Reproduzir</string>
<string name="lbl_shuffle">Embaralhar</string> <string name="lbl_shuffle">Embaralhar</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Tot</string> <string name="lbl_filter_all">Tot</string>
<string name="lbl_sort">Sortare</string> <string name="lbl_sort">Sortare</string>
<string name="lbl_sort_alpha_down">Crescător</string> <string name="lbl_sort_asc">Crescător</string>
<string name="lbl_sort_alpha_up">Descrescător</string> <string name="lbl_sort_dsc">Descrescător</string>
<string name="lbl_play">Redă</string> <string name="lbl_play">Redă</string>
<string name="lbl_shuffle">Amestecare</string> <string name="lbl_shuffle">Amestecare</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Всё</string> <string name="lbl_filter_all">Всё</string>
<string name="lbl_sort">Сортировка</string> <string name="lbl_sort">Сортировка</string>
<string name="lbl_sort_alpha_down">По возрастанию</string> <string name="lbl_sort_asc">По возрастанию</string>
<string name="lbl_sort_alpha_up">По убыванию</string> <string name="lbl_sort_dsc">По убыванию</string>
<string name="lbl_play">Воспроизвести</string> <string name="lbl_play">Воспроизвести</string>
<string name="lbl_shuffle">Перемешать</string> <string name="lbl_shuffle">Перемешать</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">Tümü</string> <string name="lbl_filter_all">Tümü</string>
<string name="lbl_sort">Sıralama</string> <string name="lbl_sort">Sıralama</string>
<string name="lbl_sort_alpha_down">Artan</string> <string name="lbl_sort_asc">Artan</string>
<string name="lbl_sort_alpha_up">Azalan</string> <string name="lbl_sort_dsc">Azalan</string>
<string name="lbl_play">Başlat</string> <string name="lbl_play">Başlat</string>
<string name="lbl_shuffle">Karıştır</string> <string name="lbl_shuffle">Karıştır</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">全部</string> <string name="lbl_filter_all">全部</string>
<string name="lbl_sort">排序方式</string> <string name="lbl_sort">排序方式</string>
<string name="lbl_sort_alpha_down">按首字符(正序)</string> <string name="lbl_sort_asc">按首字符(正序)</string>
<string name="lbl_sort_alpha_up">按首字符(倒序)</string> <string name="lbl_sort_dsc">按首字符(倒序)</string>
<string name="lbl_play">播放</string> <string name="lbl_play">播放</string>
<string name="lbl_shuffle">随机播放</string> <string name="lbl_shuffle">随机播放</string>

View file

@ -16,8 +16,8 @@
<string name="lbl_filter_all">全部</string> <string name="lbl_filter_all">全部</string>
<string name="lbl_sort">排序</string> <string name="lbl_sort">排序</string>
<string name="lbl_sort_alpha_down">升序排列</string> <string name="lbl_sort_asc">升序排列</string>
<string name="lbl_sort_alpha_up">降序排列</string> <string name="lbl_sort_dsc">降序排列</string>
<string name="lbl_play">播放</string> <string name="lbl_play">播放</string>
<string name="lbl_shuffle">隨機播放</string> <string name="lbl_shuffle">隨機播放</string>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="surface">#fafafa</color> <color name="surface">#fafafa</color>
<color name="surface_black">@android:color/black</color>
<color name="control">#202020</color> <color name="control">#202020</color>
<color name="nav_bar">#01fafafa</color> <color name="nav_bar">#01fafafa</color>

View file

@ -23,8 +23,8 @@
<string name="lbl_filter_all">All</string> <string name="lbl_filter_all">All</string>
<string name="lbl_sort">Sort</string> <string name="lbl_sort">Sort</string>
<string name="lbl_sort_alpha_down">Ascending</string> <string name="lbl_sort_asc">Ascending</string>
<string name="lbl_sort_alpha_up">Descending</string> <string name="lbl_sort_dsc">Descending</string>
<string name="lbl_sort_artist">Artist</string> <string name="lbl_sort_artist">Artist</string>
<string name="lbl_sort_album">Album</string> <string name="lbl_sort_album">Album</string>
<string name="lbl_sort_year">Year</string> <string name="lbl_sort_year">Year</string>

View file

@ -36,7 +36,7 @@
<!-- Black theme dialog theme --> <!-- Black theme dialog theme -->
<style name="Theme.CustomDialog.Black" parent="Theme.CustomDialog.Base"> <style name="Theme.CustomDialog.Black" parent="Theme.CustomDialog.Base">
<item name="colorSurface">@color/surface_black</item> <item name="colorSurface">@android:color/black</item>
</style> </style>
<!-- Material-specific dialog style --> <!-- Material-specific dialog style -->

View file

@ -9,7 +9,7 @@
<item name="android:elevation">@dimen/elevation_normal</item> <item name="android:elevation">@dimen/elevation_normal</item>
<item name="android:contentDescription">@string/desc_play_pause</item> <item name="android:contentDescription">@string/desc_play_pause</item>
<item name="android:tint">?attr/colorSurface</item> <item name="android:tint">?attr/colorSurface</item>
<item name="android:src">@drawable/ic_playing_state</item> <item name="android:src">@drawable/sel_playing_state</item>
<item name="android:layout_marginStart">@dimen/spacing_large</item> <item name="android:layout_marginStart">@dimen/spacing_large</item>
<item name="android:layout_marginTop">@dimen/spacing_medium</item> <item name="android:layout_marginTop">@dimen/spacing_medium</item>
<item name="android:layout_marginEnd">@dimen/spacing_large</item> <item name="android:layout_marginEnd">@dimen/spacing_large</item>

View file

@ -43,7 +43,7 @@
<!-- The basic black theme derived in all black accents. --> <!-- The basic black theme derived in all black accents. -->
<style name="Theme.Base.Black" parent="Theme.Base"> <style name="Theme.Base.Black" parent="Theme.Base">
<item name="colorSurface">@color/surface_black</item> <item name="colorSurface">@android:color/black</item>
<item name="materialAlertDialogTheme">@style/Theme.CustomDialog.Black</item> <item name="materialAlertDialogTheme">@style/Theme.CustomDialog.Black</item>
</style> </style>

View file

@ -9,7 +9,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.1' classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"