list: add type-specific menu impls

Add menus for each fundamental music datatype. Each will display
information about the data contained.
This commit is contained in:
Alexander Capehart 2023-07-03 15:17:47 -06:00
parent c1158b1a07
commit db2e9e12f0
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 361 additions and 27 deletions

View file

@ -22,34 +22,71 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.view.children
import org.oxycblt.auxio.R
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.DialogMenuBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/**
* A [ViewBindingBottomSheetDialogFragment] that displays basic music information and
* a series of options.
* A [ViewBindingBottomSheetDialogFragment] that displays basic music information and a series of
* options.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MenuDialogFragment : ViewBindingBottomSheetDialogFragment<DialogMenuBinding>() {
private val menuAdapter = MenuOptionAdapter()
abstract class MenuDialogFragment<T : Music> :
ViewBindingBottomSheetDialogFragment<DialogMenuBinding>(), ClickableListListener<MenuItem> {
protected abstract val menuModel: MenuViewModel
private val menuAdapter = MenuOptionAdapter(@Suppress("LeakingThis") this)
abstract val menuRes: Int
abstract val uid: Music.UID
abstract fun updateMusic(binding: DialogMenuBinding, music: T)
abstract fun onClick(music: T, @IdRes optionId: Int)
override fun onCreateBinding(inflater: LayoutInflater) = DialogMenuBinding.inflate(inflater)
override fun onBindingCreated(binding: DialogMenuBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.menuRecycler.apply {
// --- UI SETUP ---
binding.menuOptionRecycler.apply {
adapter = menuAdapter
itemAnimator = null
}
// Avoid having to use a dummy view and rely on what AndroidX Toolbar uses.
@SuppressLint("RestrictedApi") val builder = MenuBuilder(requireContext())
MenuInflater(requireContext()).inflate(R.menu.item_song, builder)
MenuInflater(requireContext()).inflate(menuRes, builder)
menuAdapter.update(builder.children.toList(), UpdateInstructions.Diff)
// --- VIEWMODEL SETUP ---
menuModel.setMusic(uid)
collectImmediately(menuModel.currentMusic, this::updateMusic)
}
override fun onDestroyBinding(binding: DialogMenuBinding) {
super.onDestroyBinding(binding)
binding.menuOptionRecycler.adapter = null
}
private fun updateMusic(music: Music?) {
if (music == null) {
logD("No music to show, navigating away")
findNavController().navigateUp()
}
@Suppress("UNCHECKED_CAST") updateMusic(requireBinding(), music as T)
}
final override fun onClick(item: MenuItem, viewHolder: RecyclerView.ViewHolder) {
@Suppress("UNCHECKED_CAST") onClick(menuModel.currentMusic.value as T, item.itemId)
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2023 Auxio Project
* MenuDialogFragmentImpl.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.list.menu
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMenuBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.util.getPlural
@AndroidEntryPoint
class SongMenuDialogFragment : MenuDialogFragment<Song>() {
override val menuModel: MenuViewModel by viewModels()
private val args: SongMenuDialogFragmentArgs by navArgs()
override val menuRes = args.menuRes
override val uid = args.songUid
override fun updateMusic(binding: DialogMenuBinding, music: Song) {
val context = requireContext()
binding.menuCover.bind(music)
binding.menuInfo.text = getString(R.string.lbl_song)
binding.menuName.text = music.name.resolve(context)
binding.menuInfo.text = music.artists.resolveNames(context)
}
override fun onClick(music: Song, optionId: Int) {}
}
@AndroidEntryPoint
class AlbumMenuDialogFragment : MenuDialogFragment<Album>() {
override val menuModel: MenuViewModel by viewModels()
private val args: AlbumMenuDialogFragmentArgs by navArgs()
override val menuRes = args.menuRes
override val uid = args.albumUid
override fun updateMusic(binding: DialogMenuBinding, music: Album) {
val context = requireContext()
binding.menuCover.bind(music)
binding.menuInfo.text = getString(music.releaseType.stringRes)
binding.menuName.text = music.name.resolve(context)
binding.menuInfo.text = music.artists.resolveNames(context)
}
override fun onClick(music: Album, optionId: Int) {}
}
@AndroidEntryPoint
class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
override val menuModel: MenuViewModel by viewModels()
private val args: ArtistMenuDialogFragmentArgs by navArgs()
override val menuRes = args.menuRes
override val uid = args.artistUid
override fun updateMusic(binding: DialogMenuBinding, music: Artist) {
val context = requireContext()
binding.menuCover.bind(music)
binding.menuInfo.text = getString(R.string.lbl_artist)
binding.menuName.text = music.name.resolve(context)
binding.menuInfo.text =
getString(
R.string.fmt_two,
context.getPlural(R.plurals.fmt_album_count, music.albums.size),
if (music.songs.isNotEmpty()) {
context.getPlural(R.plurals.fmt_song_count, music.songs.size)
} else {
getString(R.string.def_song_count)
})
}
override fun onClick(music: Artist, optionId: Int) {}
}
@AndroidEntryPoint
class GenreMenuDialogFragment : MenuDialogFragment<Genre>() {
override val menuModel: MenuViewModel by viewModels()
private val args: GenreMenuDialogFragmentArgs by navArgs()
override val menuRes = args.menuRes
override val uid = args.genreUid
override fun updateMusic(binding: DialogMenuBinding, music: Genre) {
val context = requireContext()
binding.menuCover.bind(music)
binding.menuInfo.text = getString(R.string.lbl_genre)
binding.menuName.text = music.name.resolve(context)
binding.menuInfo.text =
getString(
R.string.fmt_two,
context.getPlural(R.plurals.fmt_artist_count, music.artists.size),
context.getPlural(R.plurals.fmt_song_count, music.songs.size))
}
override fun onClick(music: Genre, optionId: Int) {}
}
@AndroidEntryPoint
class PlaylistMenuDialogFragment : MenuDialogFragment<Playlist>() {
override val menuModel: MenuViewModel by viewModels()
private val args: PlaylistMenuDialogFragmentArgs by navArgs()
override val menuRes = args.menuRes
override val uid = args.playlistUid
override fun updateMusic(binding: DialogMenuBinding, music: Playlist) {
val context = requireContext()
binding.menuCover.bind(music)
binding.menuInfo.text = getString(R.string.lbl_genre)
binding.menuName.text = music.name.resolve(context)
binding.menuInfo.text = context.getPlural(R.plurals.fmt_song_count, music.songs.size)
}
override fun onClick(music: Playlist, optionId: Int) {}
}

View file

@ -22,15 +22,18 @@ import android.view.MenuItem
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.databinding.ItemMenuOptionBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.util.inflater
/**
* Displays a list of [MenuItem]s as custom list items.
*
* @param listener A [MenuOptionAdapter] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class MenuOptionAdapter :
class MenuOptionAdapter(private val listener: ClickableListListener<MenuItem>) :
FlexibleListAdapter<MenuItem, MenuOptionViewHolder>(MenuOptionViewHolder.DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
MenuOptionViewHolder.from(parent)
@ -42,6 +45,7 @@ class MenuOptionAdapter :
/**
* A [DialogRecyclerView.ViewHolder] that displays a list of menu options based on [MenuItem].
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MenuOptionViewHolder private constructor(private val binding: ItemMenuOptionBinding) :

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 Auxio Project
* MenuViewModel.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.list.menu
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.util.logW
@HiltViewModel
class MenuViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _currentMusic = MutableStateFlow<Music?>(null)
val currentMusic: StateFlow<Music?> = _currentMusic
init {
musicRepository.addUpdateListener(this)
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
_currentMusic.value = _currentMusic.value?.let { musicRepository.find(it.uid) }
}
override fun onCleared() {
musicRepository.removeUpdateListener(this)
}
fun setMusic(uid: Music.UID) {
_currentMusic.value = musicRepository.find(uid)
if (_currentMusic.value == null) {
logW("Given Music UID to show was invalid")
}
}
}

View file

@ -1,24 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout 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"
style="@style/Widget.Auxio.RecyclerView.Linear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/menu_cover"
style="@style/Widget.Auxio.Image.MidLarge"
android:layout_margin="@dimen/spacing_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="match_parent"
android:id="@+id/menu_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/spacing_medium"
android:paddingHorizontal="@dimen/spacing_medium"
android:textAppearance="@style/TextAppearance.Auxio.HeadlineSmall"
android:text="TODO: Put info here" />
android:textAppearance="@style/TextAppearance.Auxio.LabelLarge"
android:textColor="?attr/colorSecondary"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/menu_name"
app:layout_constraintStart_toEndOf="@+id/menu_cover"
app:layout_constraintTop_toTopOf="@+id/menu_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Type" />
<TextView
android:id="@+id/menu_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Auxio.TitleLarge"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/menu_info"
app:layout_constraintStart_toStartOf="@+id/menu_type"
app:layout_constraintTop_toBottomOf="@+id/menu_type"
tools:text="Name" />
<TextView
android:id="@+id/menu_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/menu_cover"
app:layout_constraintStart_toStartOf="@+id/menu_name"
app:layout_constraintTop_toBottomOf="@+id/menu_name"
tools:text="Info A" />
<org.oxycblt.auxio.list.recycler.DialogRecyclerView
android:id="@+id/menu_recycler"
android:id="@+id/menu_option_recycler"
style="@style/Widget.Auxio.RecyclerView.Linear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/menu_cover"
tools:listitem="@layout/item_menu_option" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -30,9 +30,6 @@
<action
android:id="@+id/show_genre"
app:destination="@id/genre_detail_fragment" />
<action
android:id="@+id/open_menu"
app:destination="@id/menu_dialog" />
<action
android:id="@+id/show_playlist"
app:destination="@id/playlist_detail_fragment" />
@ -301,10 +298,69 @@
</dialog>
<dialog
android:id="@+id/menu_dialog"
android:name="org.oxycblt.auxio.list.menu.MenuDialogFragment"
android:label="menu_dialog"
tools:layout="@layout/dialog_menu" />
android:id="@+id/song_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.SongMenuDialogFragment"
android:label="song_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="menuRes"
app:argType="integer" />
<argument
android:name="songUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
android:id="@+id/album_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.AlbumMenuDialogFragment"
android:label="album_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="menuRes"
app:argType="integer"/>
<argument
android:name="albumUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
android:id="@+id/artist_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.ArtistMenuDialogFragment"
android:label="artist_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="menuRes"
app:argType="integer" />
<argument
android:name="artistUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
android:id="@+id/genre_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.GenreMenuDialogFragment"
android:label="genre_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="menuRes"
app:argType="integer" />
<argument
android:name="genreUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<dialog
android:id="@+id/playlist_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.PlaylistMenuDialogFragment"
android:label="playlist_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="menuRes"
app:argType="integer" />
<argument
android:name="playlistUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
</dialog>
<fragment
android:id="@+id/root_preferences_fragment"

View file

@ -11,7 +11,8 @@
<!-- Size Namespace | Width & Heights for UI elements -->
<dimen name="size_cover_compact">48dp</dimen>
<dimen name="size_cover_normal">56dp</dimen>
<dimen name="size_cover_medium">56dp</dimen>
<dimen name="size_cover_mid_large">92dp</dimen>
<dimen name="size_cover_large">128dp</dimen>
<dimen name="size_cover_mid_huge">192dp</dimen>
<dimen name="size_cover_huge">256dp</dimen>

View file

@ -20,6 +20,7 @@
<string name="lbl_grant">Grant</string>
<string name="lbl_songs">Songs</string>
<string name="lbl_song">Song</string>
<string name="lbl_all_songs">All songs</string>
<string name="lbl_albums">Albums</string>

View file

@ -60,8 +60,14 @@
</style>
<style name="Widget.Auxio.Image.Medium" parent="">
<item name="android:layout_width">@dimen/size_cover_normal</item>
<item name="android:layout_height">@dimen/size_cover_normal</item>
<item name="android:layout_width">@dimen/size_cover_medium</item>
<item name="android:layout_height">@dimen/size_cover_medium</item>
<item name="sizing">medium</item>
</style>
<style name="Widget.Auxio.Image.MidLarge" parent="">
<item name="android:layout_width">@dimen/size_cover_mid_large</item>
<item name="android:layout_height">@dimen/size_cover_mid_large</item>
<item name="sizing">medium</item>
</style>