detail: refactor module

Completely refactor the detail module. This is for a few reasons:

- Prevent data regeneration every time a fragment re-creates.
- Make DetailModel follow the customs of other ViewModels.
- Simplify layouts into a single detail item to reduce code
complexity.

Currently sorting doesn't work, but that is still being worked out
as the legacy SortMode continues to be phased out of Auxio.
This commit is contained in:
OxygenCobalt 2021-09-11 17:40:19 -06:00
parent d8c0037b10
commit 0e0be19e1d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
65 changed files with 694 additions and 1740 deletions

View file

@ -139,7 +139,7 @@ class MainActivity : AppCompatActivity() {
systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
setOnApplyWindowInsetsListener { v, insets ->
setOnApplyWindowInsetsListener { _, insets ->
updatePadding(
left = insets.systemWindowInsetLeft,
right = insets.systemWindowInsetRight

View file

@ -106,7 +106,7 @@ data class Accent(
val hex = context.getString(color).uppercase()
return HtmlCompat.fromHtml(
context.getString(R.string.format_accent_summary, name, hex),
context.getString(R.string.fmt_accent_desc, name, hex),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
}

View file

@ -67,6 +67,10 @@ fun ImageView.bindArtistImage(artist: Artist?) {
@BindingAdapter("genreImage")
fun ImageView.bindGenreImage(genre: Genre?) {
load(genre, R.drawable.ic_genre, MosaicFetcher(context))
if (genre != null) {
contentDescription = context.getString(R.string.desc_genre_image, genre.name)
}
}
/**

View file

@ -27,11 +27,11 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearSmoothScroller
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu
@ -52,20 +52,10 @@ class AlbumDetailFragment : DetailFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// If DetailViewModel isn't already storing the album, get it from MusicStore
// using the ID given by the navigation arguments.
if (detailModel.currentAlbum.value == null ||
detailModel.currentAlbum.value?.id != args.albumId
) {
detailModel.updateAlbum(
MusicStore.getInstance().albums.find {
it.id == args.albumId
}!!
)
}
detailModel.setAlbum(args.albumId, requireContext())
val detailAdapter = AlbumDetailAdapter(
detailModel, playbackModel, viewLifecycleOwner,
playbackModel, detailModel,
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) }
)
@ -76,7 +66,7 @@ class AlbumDetailFragment : DetailFragment() {
setupToolbar(R.menu.menu_album_detail) { itemId ->
if (itemId == R.id.action_queue_add) {
playbackModel.addToUserQueue(detailModel.currentAlbum.value!!)
playbackModel.addToUserQueue(detailModel.curAlbum.value!!)
requireContext().showToast(R.string.lbl_queue_added)
true
} else {
@ -85,19 +75,13 @@ class AlbumDetailFragment : DetailFragment() {
}
setupRecycler(detailAdapter) { pos ->
pos == 0
val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Album
}
// -- DETAILVIEWMODEL SETUP ---
detailModel.albumSortMode.observe(viewLifecycleOwner) { mode ->
logD("Updating sort mode to $mode")
// Detail header data is included
val data = mutableListOf<BaseModel>(detailModel.currentAlbum.value!!).also {
it.addAll(mode.getSortedSongList(detailModel.currentAlbum.value!!.songs))
}
detailModel.albumData.observe(viewLifecycleOwner) { data ->
detailAdapter.submitList(data)
}
@ -106,10 +90,10 @@ class AlbumDetailFragment : DetailFragment() {
// Songs should be scrolled to if the album matches, or a new detail
// fragment should be launched otherwise.
is Song -> {
if (detailModel.currentAlbum.value!!.id == item.album.id) {
scrollToItem(item.id)
if (detailModel.curAlbum.value!!.id == item.album.id) {
scrollToItem(item.id, detailAdapter)
detailModel.doneWithNavToItem()
detailModel.finishNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(item.album.id)
@ -120,9 +104,9 @@ class AlbumDetailFragment : DetailFragment() {
// If the album matches, no need to do anything. Otherwise launch a new
// detail fragment.
is Album -> {
if (detailModel.currentAlbum.value!!.id == item.id) {
if (detailModel.curAlbum.value!!.id == item.id) {
binding.detailRecycler.scrollToPosition(0)
detailModel.doneWithNavToItem()
detailModel.finishNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(item.id)
@ -146,7 +130,7 @@ class AlbumDetailFragment : DetailFragment() {
playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_ALBUM &&
playbackModel.parent.value?.id == detailModel.currentAlbum.value!!.id
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id
) {
detailAdapter.highlightSong(song, binding.detailRecycler)
} else {
@ -169,17 +153,15 @@ class AlbumDetailFragment : DetailFragment() {
/**
* Scroll to an song using its [id].
*/
private fun scrollToItem(id: Long) {
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) {
// Calculate where the item for the currently played song is
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
detailModel.currentAlbum.value!!.songs
).indexOfFirst { it.id == id }
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Album }
if (pos != -1) {
binding.detailRecycler.post {
// Make sure to increment the position to make up for the detail header
binding.detailRecycler.layoutManager?.startSmoothScroll(
CenterSmoothScroller(requireContext(), pos.inc())
CenterSmoothScroller(requireContext(), pos)
)
// If the recyclerview can scroll, its certain that it will have to scroll to

View file

@ -24,17 +24,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.logD
@ -50,20 +47,10 @@ class ArtistDetailFragment : DetailFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// If DetailViewModel isn't already storing the artist, get it from MusicStore
// using the ID given by the navigation arguments
if (detailModel.currentArtist.value == null ||
detailModel.currentArtist.value?.id != args.artistId
) {
detailModel.updateArtist(
MusicStore.getInstance().artists.find {
it.id == args.artistId
}!!
)
}
detailModel.setArtist(args.artistId, requireContext())
val detailAdapter = ArtistDetailAdapter(
playbackModel, detailModel,
playbackModel,
doOnClick = { data ->
if (!detailModel.isNavigating) {
detailModel.setNavigating(true)
@ -88,39 +75,22 @@ class ArtistDetailFragment : DetailFragment() {
setupToolbar()
setupRecycler(detailAdapter) { pos ->
// If the item is an ActionHeader we need to also make the item full-width
pos == 0 || detailAdapter.currentList.getOrNull(pos) is Header
val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Artist
}
// --- VIEWMODEL SETUP ---
detailModel.artistSortMode.observe(viewLifecycleOwner) { mode ->
logD("Updating sort mode to $mode")
val artist = detailModel.currentArtist.value!!
val data = mutableListOf<BaseModel>(artist)
data.addAll(SortMode.NUMERIC_DOWN.getSortedAlbumList(artist.albums))
data.add(
Header(
id = -2,
name = getString(R.string.lbl_songs),
isAction = true
)
)
data.addAll(mode.getSortedArtistSongList(artist.songs))
detailModel.artistData.observe(viewLifecycleOwner) { data ->
detailAdapter.submitList(data)
}
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) {
is Artist -> {
if (item.id == detailModel.currentArtist.value!!.id) {
if (item.id == detailModel.curArtist.value?.id) {
binding.detailRecycler.scrollToPosition(0)
detailModel.doneWithNavToItem()
detailModel.finishNavToItem()
} else {
findNavController().navigate(
ArtistDetailFragmentDirections.actionShowArtist(item.id)
@ -136,7 +106,8 @@ class ArtistDetailFragment : DetailFragment() {
ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)
)
else -> {}
else -> {
}
}
}
@ -152,7 +123,7 @@ class ArtistDetailFragment : DetailFragment() {
// Highlight songs if they are being played
playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_ARTIST &&
playbackModel.parent.value?.id == detailModel.currentArtist.value!!.id
playbackModel.parent.value?.id == detailModel.curArtist.value?.id
) {
detailAdapter.highlightSong(song, binding.detailRecycler)
} else {

View file

@ -36,9 +36,6 @@ import org.oxycblt.auxio.util.isLandscape
/**
* A Base [Fragment] implementing the base features shared across all detail fragments.
* TODO: Want to implement something using CollapsingToolbarLayout. This would eliminate alot of
* the complexity from this ball of mud, but I have to do something to fix the scroll stopping
* issue.
* @author OxygenCobalt
*/
abstract class DetailFragment : Fragment() {

View file

@ -18,112 +18,135 @@
package org.oxycblt.auxio.detail
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.LibSortMode
import org.oxycblt.auxio.music.ActionHeader
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.settings.SettingsManager
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.ui.SortMode
/**
* ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what
* [SortMode] they are currently on.
* TODO: Redo sorting here
* TODO: Re-add sorting
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel() {
private val settingsManager = SettingsManager.getInstance()
// --- CURRENT VALUES ---
private val mCurrentGenre = MutableLiveData<Genre?>()
val currentGenre: LiveData<Genre?> get() = mCurrentGenre
private val mCurGenre = MutableLiveData<Genre?>()
val curGenre: LiveData<Genre?> get() = mCurGenre
private val mCurrentArtist = MutableLiveData<Artist?>()
val currentArtist: LiveData<Artist?> get() = mCurrentArtist
private val mGenreData = MutableLiveData(listOf<BaseModel>())
val genreData: LiveData<List<BaseModel>> = mGenreData
private val mCurrentAlbum = MutableLiveData<Album?>()
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
private val mCurArtist = MutableLiveData<Artist?>()
val curArtist: LiveData<Artist?> get() = mCurArtist
// --- SORT MODES ---
private val mArtistData = MutableLiveData(listOf<BaseModel>())
val artistData: LiveData<List<BaseModel>> = mArtistData
private val mGenreSortMode = MutableLiveData(settingsManager.genreSortMode)
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
private val mCurAlbum = MutableLiveData<Album?>()
val curAlbum: LiveData<Album?> get() = mCurAlbum
private val mArtistSortMode = MutableLiveData(settingsManager.artistSortMode)
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
private val mAlbumData = MutableLiveData(listOf<BaseModel>())
val albumData: LiveData<List<BaseModel>> get() = mAlbumData
private val mAlbumSortMode = MutableLiveData(settingsManager.albumSortMode)
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating
var isNavigating = false
private set
private val mNavToItem = MutableLiveData<BaseModel?>()
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
val navToItem: LiveData<BaseModel?> get() = mNavToItem
fun updateGenre(genre: Genre) {
mCurrentGenre.value = genre
private val musicStore = MusicStore.getInstance()
fun setGenre(id: Long, context: Context) {
if (mCurGenre.value?.id == id) return
mCurGenre.value = musicStore.genres.find { it.id == id }
val data = mutableListOf<BaseModel>(curGenre.value!!)
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
}
)
)
data.addAll(LibSortMode.ASCENDING.sortGenre(curGenre.value!!))
mGenreData.value = data
}
fun updateArtist(artist: Artist) {
mCurrentArtist.value = artist
fun setArtist(id: Long, context: Context) {
if (mCurArtist.value?.id == id) return
mCurArtist.value = musicStore.artists.find { it.id == id }
val artist = curArtist.value!!
val data = mutableListOf<BaseModel>(artist)
data.add(
Header(
id = -2,
name = context.getString(R.string.lbl_albums)
)
)
data.addAll(LibSortMode.YEAR.sortAlbums(artist.albums))
data.add(
ActionHeader(
id = -3,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
}
)
)
data.addAll(LibSortMode.YEAR.sortArtist(artist))
mArtistData.value = data.toList()
}
fun updateAlbum(album: Album) {
mCurrentAlbum.value = album
}
fun setAlbum(id: Long, context: Context) {
if (mCurAlbum.value?.id == id) return
/**
* Increment the sort mode of the genre artists
*/
fun incrementGenreSortMode() {
val mode = when (mGenreSortMode.value) {
SortMode.ALPHA_DOWN -> SortMode.ALPHA_UP
SortMode.ALPHA_UP -> SortMode.ALPHA_DOWN
mCurAlbum.value = musicStore.albums.find { it.id == id }
else -> SortMode.ALPHA_DOWN
}
val data = mutableListOf<BaseModel>(curAlbum.value!!)
mGenreSortMode.value = mode
settingsManager.genreSortMode = mode
}
data.add(
ActionHeader(
id = -2,
name = context.getString(R.string.lbl_songs),
icon = R.drawable.ic_sort,
desc = R.string.lbl_sort,
onClick = {
}
)
)
/**
* Increment the sort mode of the artist albums
*/
fun incrementArtistSortMode() {
val mode = when (mArtistSortMode.value) {
SortMode.NUMERIC_DOWN -> SortMode.NUMERIC_UP
SortMode.NUMERIC_UP -> SortMode.ALPHA_DOWN
SortMode.ALPHA_DOWN -> SortMode.ALPHA_UP
SortMode.ALPHA_UP -> SortMode.NUMERIC_DOWN
data.addAll(LibSortMode.ASCENDING.sortAlbum(curAlbum.value!!))
else -> SortMode.NUMERIC_DOWN
}
mArtistSortMode.value = mode
settingsManager.artistSortMode = mode
}
/**
* Increment the sort mode of the album song
*/
fun incrementAlbumSortMode() {
val mode = when (mAlbumSortMode.value) {
SortMode.NUMERIC_DOWN -> SortMode.NUMERIC_UP
SortMode.NUMERIC_UP -> SortMode.NUMERIC_DOWN
else -> SortMode.NUMERIC_DOWN
}
mAlbumSortMode.value = mode
settingsManager.albumSortMode = mAlbumSortMode.value!!
mAlbumData.value = data
}
/**
@ -136,14 +159,14 @@ class DetailViewModel : ViewModel() {
/**
* Mark that the navigation process is done.
*/
fun doneWithNavToItem() {
fun finishNavToItem() {
mNavToItem.value = null
}
/**
* Update the current navigation status to [isNavigating]
*/
fun setNavigating(isNavigating: Boolean) {
mIsNavigating = isNavigating
fun setNavigating(navigating: Boolean) {
isNavigating = navigating
}
}

View file

@ -24,11 +24,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.detail.adapters.GenreDetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu
@ -47,20 +48,10 @@ class GenreDetailFragment : DetailFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// If DetailViewModel isn't already storing the genre, get it from MusicStore
// using the ID given by the navigation arguments
if (detailModel.currentGenre.value == null ||
detailModel.currentGenre.value?.id != args.genreId
) {
detailModel.updateGenre(
MusicStore.getInstance().genres.find {
it.id == args.genreId
}!!
)
}
detailModel.setGenre(args.genreId, requireContext())
val detailAdapter = GenreDetailAdapter(
detailModel, playbackModel, viewLifecycleOwner,
playbackModel,
doOnClick = { song ->
playbackModel.playSong(song, PlaybackMode.IN_GENRE)
},
@ -75,19 +66,13 @@ class GenreDetailFragment : DetailFragment() {
setupToolbar()
setupRecycler(detailAdapter) { pos ->
pos == 0
val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Genre
}
// --- DETAILVIEWMODEL SETUP ---
detailModel.genreSortMode.observe(viewLifecycleOwner) { mode ->
logD("Updating sort mode to $mode")
// Detail header data is included
val data = mutableListOf<BaseModel>(detailModel.currentGenre.value!!).also {
it.addAll(mode.getSortedSongList(detailModel.currentGenre.value!!.songs))
}
detailModel.genreData.observe(viewLifecycleOwner) { data ->
detailAdapter.submitList(data)
}
@ -106,7 +91,8 @@ class GenreDetailFragment : DetailFragment() {
GenreDetailFragmentDirections.actionShowAlbum(item.album.id)
)
else -> {}
else -> {
}
}
}
@ -114,7 +100,7 @@ class GenreDetailFragment : DetailFragment() {
playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_GENRE &&
playbackModel.parent.value?.id == detailModel.currentGenre.value!!.id
playbackModel.parent.value?.id == detailModel.curGenre.value!!.id
) {
detailAdapter.highlightSong(song, binding.detailRecycler)
} else {

View file

@ -16,23 +16,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail.adapters
package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemAlbumHeaderBinding
import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindAlbumArt
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.disable
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
@ -40,9 +43,8 @@ import org.oxycblt.auxio.util.inflater
* @author OxygenCobalt
*/
class AlbumDetailAdapter(
private val detailModel: DetailViewModel,
private val playbackModel: PlaybackViewModel,
private val lifecycleOwner: LifecycleOwner,
private val detailModel: DetailViewModel,
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (view: View, data: Song) -> Unit
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback()) {
@ -52,20 +54,25 @@ class AlbumDetailAdapter(
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Album -> ALBUM_HEADER_ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
is Song -> ALBUM_SONG_ITEM_TYPE
else -> -1
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ALBUM_HEADER_ITEM_TYPE -> AlbumHeaderViewHolder(
ItemAlbumHeaderBinding.inflate(parent.context.inflater)
ItemDetailBinding.inflate(parent.context.inflater)
)
ALBUM_SONG_ITEM_TYPE -> AlbumSongViewHolder(
ItemAlbumSongBinding.inflate(parent.context.inflater)
)
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
else -> error("Invalid ViewHolder item type $viewType")
}
}
@ -76,18 +83,20 @@ class AlbumDetailAdapter(
when (item) {
is Album -> (holder as AlbumHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
else -> {}
else -> {
}
}
if (currentSong != null && position > 0) {
if (holder is Highlightable) {
if (item.id == currentSong?.id) {
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
currentHolder?.setHighlighted(false)
currentHolder = (holder as Highlightable)
currentHolder = holder
holder.setHighlighted(true)
} else {
(holder as Highlightable).setHighlighted(false)
holder.setHighlighted(false)
}
}
}
@ -124,17 +133,41 @@ class AlbumDetailAdapter(
}
inner class AlbumHeaderViewHolder(
private val binding: ItemAlbumHeaderBinding
private val binding: ItemDetailBinding
) : BaseViewHolder<Album>(binding) {
override fun onBind(data: Album) {
binding.album = data
binding.detailModel = detailModel
binding.playbackModel = playbackModel
binding.lifecycleOwner = lifecycleOwner
binding.detailCover.apply {
bindAlbumArt(data)
contentDescription = context.getString(R.string.desc_album_cover, data.name)
}
if (data.songs.size < 2) {
binding.albumSortButton.disable()
binding.detailName.text = data.name
binding.detailSubhead.apply {
text = data.artist.name
setOnClickListener {
detailModel.navToItem(data)
}
}
binding.detailInfo.text = binding.detailInfo.context.getString(
R.string.fmt_three,
data.year.toString(),
binding.detailInfo.context.getPlural(
R.plurals.fmt_song_count,
data.songs.size
),
data.totalDuration
)
binding.detailPlayButton.setOnClickListener {
playbackModel.playAlbum(data, false)
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.playAlbum(data, true)
}
}
}

View file

@ -16,36 +16,37 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail.adapters
package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindArtistImage
import org.oxycblt.auxio.databinding.ItemArtistAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistHeaderBinding
import org.oxycblt.auxio.databinding.ItemArtistSongBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.disable
import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
* An adapter for displaying the [Album]s and [Song]s of an artist.
* This isnt the nicest implementation, but it works.
* @author OxygenCobalt
*/
class ArtistDetailAdapter(
private val playbackModel: PlaybackViewModel,
private val detailModel: DetailViewModel,
private val doOnClick: (data: Album) -> Unit,
private val doOnSongClick: (data: Song) -> Unit,
private val doOnLongClick: (view: View, data: BaseModel) -> Unit,
@ -60,8 +61,9 @@ class ArtistDetailAdapter(
return when (getItem(position)) {
is Artist -> ARTIST_HEADER_ITEM_TYPE
is Album -> ARTIST_ALBUM_ITEM_TYPE
is Header -> ARTIST_SONG_HEADER_ITEM_TYPE
is Song -> ARTIST_SONG_ITEM_TYPE
is Header -> HeaderViewHolder.ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
else -> -1
}
@ -70,21 +72,21 @@ class ArtistDetailAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ARTIST_HEADER_ITEM_TYPE -> ArtistHeaderViewHolder(
ItemArtistHeaderBinding.inflate(parent.context.inflater)
ItemDetailBinding.inflate(parent.context.inflater)
)
ARTIST_ALBUM_ITEM_TYPE -> ArtistAlbumViewHolder(
ItemArtistAlbumBinding.inflate(parent.context.inflater)
)
ARTIST_SONG_HEADER_ITEM_TYPE -> ArtistSongHeaderViewHolder(
ItemActionHeaderBinding.inflate(parent.context.inflater)
)
ARTIST_SONG_ITEM_TYPE -> ArtistSongViewHolder(
ItemArtistSongBinding.inflate(parent.context.inflater)
)
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
else -> error("Invalid ViewHolder item type $viewType")
}
}
@ -95,9 +97,9 @@ class ArtistDetailAdapter(
when (item) {
is Artist -> (holder as ArtistHeaderViewHolder).bind(item)
is Album -> (holder as ArtistAlbumViewHolder).bind(item)
is Header -> (holder as ArtistSongHeaderViewHolder).bind(item)
is Song -> (holder as ArtistSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
else -> {
}
}
@ -180,22 +182,43 @@ class ArtistDetailAdapter(
}
inner class ArtistHeaderViewHolder(
private val binding: ItemArtistHeaderBinding
private val binding: ItemDetailBinding
) : BaseViewHolder<Artist>(binding) {
override fun onBind(data: Artist) {
binding.artist = data
binding.playbackModel = playbackModel
val context = binding.root.context
binding.detailCover.apply {
bindArtistImage(data)
contentDescription = context.getString(R.string.desc_artist_image, data.name)
}
binding.detailName.text = data.name
binding.detailSubhead.text = data.genre?.resolvedName
?: context.getString(R.string.def_genre)
binding.detailInfo.text = context.getString(
R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, data.albums.size),
context.getPlural(R.plurals.fmt_song_count, data.songs.size)
)
binding.detailPlayButton.setOnClickListener {
playbackModel.playArtist(data, false)
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.playArtist(data, true)
}
}
}
// Generic ViewHolder for a detail album
inner class ArtistAlbumViewHolder(
private val binding: ItemArtistAlbumBinding,
) : BaseViewHolder<Album>(binding, doOnClick, doOnLongClick), Highlightable {
override fun onBind(data: Album) {
binding.album = data
binding.albumName.requestLayout()
}
@ -204,39 +227,11 @@ class ArtistDetailAdapter(
}
}
inner class ArtistSongHeaderViewHolder(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<Header>(binding) {
override fun onBind(data: Header) {
binding.header = data
binding.headerButton.apply {
val sortMode = detailModel.artistSortMode
val artist = detailModel.currentArtist.value!!
setImageResource(sortMode.value!!.iconRes)
setOnClickListener {
detailModel.incrementArtistSortMode()
setImageResource(sortMode.value!!.iconRes)
}
if (artist.songs.size < 2) {
disable()
}
}
}
}
inner class ArtistSongViewHolder(
private val binding: ItemArtistSongBinding,
) : BaseViewHolder<Song>(binding, doOnSongClick, doOnLongClick), Highlightable {
private val normalTextColor = binding.songName.currentTextColor
override fun onBind(data: Song) {
binding.song = data
binding.songName.requestLayout()
}
@ -248,7 +243,6 @@ class ArtistDetailAdapter(
companion object {
const val ARTIST_HEADER_ITEM_TYPE = 0xA009
const val ARTIST_ALBUM_ITEM_TYPE = 0xA00A
const val ARTIST_SONG_HEADER_ITEM_TYPE = 0xA00B
const val ARTIST_SONG_ITEM_TYPE = 0xA00C
}
}

View file

@ -16,23 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail.adapters
package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemGenreHeaderBinding
import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindGenreImage
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemGenreSongBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.disable
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
@ -40,9 +42,7 @@ import org.oxycblt.auxio.util.inflater
* @author OxygenCobalt
*/
class GenreDetailAdapter(
private val detailModel: DetailViewModel,
private val playbackModel: PlaybackViewModel,
private val lifecycleOwner: LifecycleOwner,
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (view: View, data: Song) -> Unit
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback()) {
@ -52,6 +52,7 @@ class GenreDetailAdapter(
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Genre -> GENRE_HEADER_ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
is Song -> GENRE_SONG_ITEM_TYPE
else -> -1
@ -61,13 +62,15 @@ class GenreDetailAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
GENRE_HEADER_ITEM_TYPE -> GenreHeaderViewHolder(
ItemGenreHeaderBinding.inflate(parent.context.inflater)
ItemDetailBinding.inflate(parent.context.inflater)
)
GENRE_SONG_ITEM_TYPE -> GenreSongViewHolder(
ItemGenreSongBinding.inflate(parent.context.inflater),
)
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
else -> error("Bad viewholder item type $viewType")
}
}
@ -77,18 +80,19 @@ class GenreDetailAdapter(
when (item) {
is Genre -> (holder as GenreHeaderViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
is Song -> (holder as GenreSongViewHolder).bind(item)
else -> {}
}
if (currentSong != null && position > 0) {
if (holder is Highlightable) {
if (item.id == currentSong?.id) {
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
currentHolder?.setHighlighted(false)
currentHolder = (holder as Highlightable)
currentHolder = holder
holder.setHighlighted(true)
} else {
(holder as Highlightable).setHighlighted(false)
holder.setHighlighted(false)
}
}
}
@ -125,16 +129,30 @@ class GenreDetailAdapter(
}
inner class GenreHeaderViewHolder(
private val binding: ItemGenreHeaderBinding
private val binding: ItemDetailBinding
) : BaseViewHolder<Genre>(binding) {
override fun onBind(data: Genre) {
binding.genre = data
binding.detailModel = detailModel
binding.playbackModel = playbackModel
binding.lifecycleOwner = lifecycleOwner
val context = binding.root.context
if (data.songs.size < 2) {
binding.genreSortButton.disable()
binding.detailCover.apply {
bindGenreImage(data)
contentDescription = context.getString(R.string.desc_artist_image, data.name)
}
binding.detailName.text = data.name
binding.detailSubhead.apply {
text = context.getPlural(R.plurals.fmt_song_count, data.songs.size)
}
binding.detailInfo.text = data.totalDuration
binding.detailPlayButton.setOnClickListener {
playbackModel.playGenre(data, false)
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.playGenre(data, true)
}
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail.adapters
package org.oxycblt.auxio.detail.recycler
/**
* Interface that allows the highlighting of certain ViewHolders

View file

@ -25,7 +25,7 @@ import org.oxycblt.auxio.databinding.ItemExcludedDirBinding
import org.oxycblt.auxio.util.inflater
/**
* Adapter that shows the blacklist entries and their "Clear" button.
* Adapter that shows the excluded directories and their "Clear" button.
* @author OxygenCobalt
*/
class ExcludedEntryAdapter(

View file

@ -21,7 +21,9 @@ 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.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.sliceArticle
@ -130,6 +132,22 @@ enum class LibSortMode(@IdRes val itemId: Int) {
}
}
/**
* Sort the songs in an artist.
* @see sortSongs
*/
fun sortArtist(artist: Artist): List<Song> {
return sortSongs(artist.songs)
}
/**
* Sort the songs in a genre.
* @see sortSongs
*/
fun sortGenre(genre: Genre): List<Song> {
return sortSongs(genre.songs)
}
companion object {
/**
* Convert a menu [id] to an instance of [LibSortMode].

View file

@ -19,6 +19,9 @@
package org.oxycblt.auxio.music
import android.net.Uri
import android.view.View
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
// --- MUSIC MODELS ---
@ -218,11 +221,21 @@ data class Genre(
}
/**
* A data object used solely for the "Header" UI element. Inherits [BaseModel].
* @param isAction Whether this header corresponds to an action or not
* A data object used solely for the "Header" UI element.
*/
data class Header(
override val id: Long,
override val name: String,
val isAction: Boolean = false
) : BaseModel()
/**
* A data object used for an action header. Like [Header], but with a button.
* Inherits [BaseModel].
*/
data class ActionHeader(
override val id: Long,
override val name: String,
@DrawableRes val icon: Int,
@StringRes val desc: Int,
val onClick: (View) -> Unit
) : BaseModel()

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.music
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.text.format.DateUtils
@ -122,67 +121,16 @@ fun Long.toDuration(): String {
return durationString
}
/**
* Convert an integer to its formatted year.
*/
fun Int.toYear(context: Context): String {
return if (this > 0) {
toString()
} else {
context.getString(R.string.def_date)
}
}
// --- BINDING ADAPTERS ---
/**
* Bind the most prominent artist genre
*/
@BindingAdapter("artistGenre")
fun TextView.bindArtistGenre(artist: Artist) {
text = artist.genre?.resolvedName ?: context.getString(R.string.def_genre)
}
/**
* Bind the album + song counts for an artist
*/
@BindingAdapter("artistCounts")
fun TextView.bindArtistCounts(artist: Artist) {
val albums = context.getPlural(R.plurals.fmt_album_count, artist.albums.size)
val songs = context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
text = context.getString(R.string.format_double_counts, albums, songs)
}
/**
* Get all album information, used on [org.oxycblt.auxio.detail.AlbumDetailFragment]
*/
@BindingAdapter("albumDetails")
fun TextView.bindAllAlbumDetails(album: Album) {
text = context.getString(
R.string.format_double_info,
album.year.toYear(context),
context.getPlural(R.plurals.fmt_song_count, album.songs.size),
album.totalDuration
R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, artist.albums.size),
context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
)
}
/**
* Get basic information about an album, used on album ViewHolders
*/
@BindingAdapter("albumInfo")
fun TextView.bindAlbumInfo(album: Album) {
text = context.getString(
R.string.format_info,
album.artist.name,
context.getPlural(R.plurals.fmt_song_count, album.songs.size),
)
}
/**
* Bind the year for an album.
*/
@BindingAdapter("albumYear")
fun TextView.bindAlbumYear(album: Album) {
text = album.year.toYear(context)
}

View file

@ -129,11 +129,11 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { nextQueue ->
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
updateQueueIcon(queueItem)
}
playbackModel.userQueue.observe(viewLifecycleOwner) { userQueue ->
playbackModel.userQueue.observe(viewLifecycleOwner) {
updateQueueIcon(queueItem)
}

View file

@ -21,17 +21,16 @@ package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder
@ -46,8 +45,7 @@ import org.oxycblt.auxio.util.logE
* @author OxygenCobalt
*/
class QueueAdapter(
private val touchHelper: ItemTouchHelper,
private val playbackModel: PlaybackViewModel
private val touchHelper: ItemTouchHelper
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data = mutableListOf<BaseModel>()
private var listDiffer = AsyncListDiffer(this, DiffCallback())
@ -55,14 +53,10 @@ class QueueAdapter(
override fun getItemCount(): Int = data.size
override fun getItemViewType(position: Int): Int {
return when (val item = data[position]) {
is Header -> if (item.isAction) {
USER_QUEUE_HEADER_ITEM_TYPE
} else {
HeaderViewHolder.ITEM_TYPE
}
return when (data[position]) {
is Song -> QUEUE_SONG_ITEM_TYPE
is Header -> HeaderViewHolder.ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
else -> -1
}
@ -70,29 +64,22 @@ class QueueAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
USER_QUEUE_HEADER_ITEM_TYPE -> UserQueueHeaderViewHolder(
ItemActionHeaderBinding.inflate(parent.context.inflater)
)
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
ItemQueueSongBinding.inflate(parent.context.inflater)
)
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
else -> error("Invalid viewholder item type $viewType.")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = data[position]) {
is Header -> if (item.isAction) {
(holder as UserQueueHeaderViewHolder).bind(item)
} else {
(holder as HeaderViewHolder).bind(item)
}
is Song -> (holder as QueueSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
else -> logE("Bad data given to QueueAdapter.")
}
@ -181,29 +168,6 @@ class QueueAdapter(
}
}
/*
* The viewholder for
*/
inner class UserQueueHeaderViewHolder(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<Header>(binding) {
override fun onBind(data: Header) {
binding.header = data
binding.headerButton.apply {
setImageResource(R.drawable.ic_clear)
contentDescription = context.getString(R.string.desc_clear_user_queue)
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener {
playbackModel.clearUserQueue()
}
}
}
}
companion object {
const val QUEUE_SONG_ITEM_TYPE = 0xA005
const val USER_QUEUE_HEADER_ITEM_TYPE = 0xA006

View file

@ -90,6 +90,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
// an elevation change.
// TODO: Maybe restrict the item from being drawn over the recycler bounds?
// Seems like its possible with enough UI magic
// TODO: Add an accented BG to the removal action
val view = viewHolder.itemView

View file

@ -30,6 +30,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import kotlinx.coroutines.NonDisposableHandle.parent
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -53,7 +54,7 @@ class QueueFragment : Fragment() {
val callback = QueueDragCallback(playbackModel)
val helper = ItemTouchHelper(callback)
val queueAdapter = QueueAdapter(helper, playbackModel)
val queueAdapter = QueueAdapter(helper)
var lastShuffle = playbackModel.isShuffling.value
callback.addQueueAdapter(queueAdapter)
@ -120,10 +121,12 @@ class QueueFragment : Fragment() {
val nextQueue = playbackModel.nextItemsInQueue.value!!
if (userQueue.isNotEmpty()) {
queue += Header(
queue += ActionHeader(
id = -2,
name = getString(R.string.lbl_next_user_queue),
isAction = true
icon = R.drawable.ic_clear,
desc = R.string.desc_clear_user_queue,
onClick = { playbackModel.clearUserQueue() }
)
queue += userQueue
@ -132,8 +135,10 @@ class QueueFragment : Fragment() {
if (nextQueue.isNotEmpty()) {
queue += Header(
id = -3,
name = getString(R.string.fmt_next_from, getParentName()),
isAction = false
name = getString(
R.string.fmt_next_from,
playbackModel.parent.value?.displayName ?: getString(R.string.lbl_all_songs)
)
)
queue += nextQueue
@ -141,8 +146,4 @@ class QueueFragment : Fragment() {
return queue
}
private fun getParentName(): String {
return playbackModel.parent.value?.displayName ?: getString(R.string.lbl_all_songs)
}
}

View file

@ -30,7 +30,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.detail.DetailViewModel
@ -74,9 +73,6 @@ class SearchFragment : Fragment() {
},
::newMenu
)
val toolbarParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams
// --- UI SETUP --
binding.lifecycleOwner = viewLifecycleOwner

View file

@ -130,7 +130,11 @@ class SearchViewModel : ViewModel() {
*/
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
val filtered = filter {
it.name.normalized().contains(value.normalized(), ignoreCase = true)
// First see if the normal item name will work. If that fails, try the "normalized"
// [e.g all accented/unicode chars become latin chars] instead. Hopefully this
// shouldn't break other language's search functionality.
it.name.contains(value, ignoreCase = true) ||
it.name.normalized().contains(value, ignoreCase = true)
}
return if (filtered.isNotEmpty()) filtered else null
@ -155,8 +159,8 @@ class SearchViewModel : ViewModel() {
idx += Character.charCount(cp)
when (Character.getType(cp)) {
Character.NON_SPACING_MARK.toInt(), Character.COMBINING_SPACING_MARK.toInt() ->
continue
// Character.NON_SPACING_MARK and Character.COMBINING_SPACING_MARK
6, 8 -> continue
else -> sb.appendCodePoint(cp)
}

View file

@ -18,14 +18,8 @@
package org.oxycblt.auxio.ui
import android.widget.ImageButton
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
/**
@ -35,83 +29,11 @@ import org.oxycblt.auxio.music.Song
*/
enum class SortMode(@DrawableRes val iconRes: Int) {
// Icons for each mode are assigned to the enums themselves
NONE(R.drawable.ic_sort_none),
ALPHA_UP(R.drawable.ic_sort_alpha_up),
ALPHA_DOWN(R.drawable.ic_sort_alpha_down),
NUMERIC_UP(R.drawable.ic_sort_numeric_up),
NUMERIC_DOWN(R.drawable.ic_sort_numeric_down);
/**
* Get a sorted list of genres for a SortMode. Only supports alphabetic sorting.
* @param genres An unsorted list of genres.
* @return The sorted list of genres.
*/
fun getSortedGenreList(genres: List<Genre>): List<Genre> {
return when (this) {
ALPHA_UP -> genres.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) {
it.resolvedName.sliceArticle()
}
)
ALPHA_DOWN -> genres.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) {
it.resolvedName.sliceArticle()
}
)
else -> genres
}
}
/**
* Get a sorted list of artists for a SortMode. Only supports alphabetic sorting.
* @param artists An unsorted list of artists.
* @return The sorted list of artists.
*/
fun getSortedArtistList(artists: List<Artist>): List<Artist> {
return when (this) {
ALPHA_UP -> artists.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) {
it.name.sliceArticle()
}
)
ALPHA_DOWN -> artists.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) {
it.name.sliceArticle()
}
)
else -> artists
}
}
/**
* Get a sorted list of albums for a SortMode. Supports alpha + numeric sorting.
* @param albums An unsorted list of albums.
* @return The sorted list of albums.
*/
fun getSortedAlbumList(albums: List<Album>): List<Album> {
return when (this) {
ALPHA_UP -> albums.sortedWith(
compareByDescending(String.CASE_INSENSITIVE_ORDER) {
it.name.sliceArticle()
}
)
ALPHA_DOWN -> albums.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) {
it.name.sliceArticle()
}
)
NUMERIC_UP -> albums.sortedBy { it.year }
NUMERIC_DOWN -> albums.sortedByDescending { it.year }
else -> albums
}
}
NONE(R.drawable.ic_sort),
ALPHA_UP(R.drawable.ic_sort),
ALPHA_DOWN(R.drawable.ic_sort),
NUMERIC_UP(R.drawable.ic_sort),
NUMERIC_DOWN(R.drawable.ic_sort);
/**
* Get a sorted list of songs for a SortMode. Supports alpha + numeric sorting.
@ -182,19 +104,6 @@ enum class SortMode(@DrawableRes val iconRes: Int) {
}
}
/**
* Get a sorting menu ID for this mode. Alphabetic only.
* @return The action id for this mode.
*/
@IdRes
fun toMenuId(): Int {
return when (this) {
ALPHA_UP -> R.id.option_sort_asc
ALPHA_DOWN -> R.id.option_sort_dsc
else -> R.id.option_sort_dsc
}
}
/**
* Get the constant for this mode. Used to write a compressed variant to SettingsManager
* @return The int constant for this mode.
@ -234,14 +143,6 @@ enum class SortMode(@DrawableRes val iconRes: Int) {
}
}
/**
* Bind the [SortMode] icon for an ImageButton.
*/
@BindingAdapter("sortIcon")
fun ImageButton.bindSortIcon(mode: SortMode) {
setImageResource(mode.iconRes)
}
/**
* Slice a string so that any preceding articles like The/A(n) are truncated.
* This is hilariously anglo-centric, but its mostly for MediaStore compat and hopefully

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2021 Auxio Project
* ViewHolders.kt is part of Auxio.
* SortHeaderViewHolder.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
@ -20,13 +20,16 @@ package org.oxycblt.auxio.ui
import android.content.Context
import android.view.View
import androidx.appcompat.widget.TooltipCompat
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding
import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
@ -245,3 +248,29 @@ class HeaderViewHolder private constructor(
}
}
}
class ActionHeaderViewHolder private constructor(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<ActionHeader>(binding) {
override fun onBind(data: ActionHeader) {
binding.header = data
binding.headerButton.apply {
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener(data.onClick)
}
}
companion object {
const val ITEM_TYPE = 0xA999 // TODO: Give this an ID
/**
* Create an instance of [ActionHeaderViewHolder]
*/
fun from(context: Context): ActionHeaderViewHolder {
return ActionHeaderViewHolder(ItemActionHeaderBinding.inflate(context.inflater))
}
}
}

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM21.41 6.34l-3.75-3.75-2.53 2.54 3.75 3.75 2.53-2.54z" />
</vector>

View file

@ -2,7 +2,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.694 17.992H0.976l4.142-12h3.27l4.136 12H9.806L6.8 8.734H6.706zm-0.17-4.717h6.422v1.98H3.524zm10.313 4.717v-1.506l5.988-8.402h-6V5.992h9.188v1.505L17.018 15.9h6.006v2.092z" />
<path
android:fillColor="@android:color/white"
android:pathData="M15.5 20.535h-7l3.5 3.456 3.5-3.456z" />
</vector>

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5 3.448h-7L12-0.008l3.5 3.456z" />
<path
android:fillColor="@android:color/white"
android:pathData="M3.694 17.992H0.976l4.142-12h3.27l4.136 12H9.806L6.8 8.734H6.706zm-0.17-4.717h6.422v1.98H3.524zm10.313 4.717v-1.506l5.988-8.402h-6V5.992h9.188v1.505L17.018 15.9h6.006v2.092z" />
</vector>

View file

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6.542 18q-1.508-0.006-2.595-0.716-1.082-0.711-1.667-2.06-0.578-1.348-0.573-3.244 0-1.89 0.58-3.221 0.584-1.331 1.665-2.025 1.088-0.7 2.59-0.7 1.503 0 2.584 0.7 1.088 0.7 1.673 2.03 0.584 1.326 0.578 3.216 0 1.902-0.584 3.25-0.579 1.348-1.66 2.06Q8.05 18 6.542 18zm0-2.025q1.03 0 1.643-0.999 0.614-0.998 0.608-2.996 0-1.314-0.28-2.188-0.275-0.875-0.784-1.315-0.503-0.44-1.187-0.44-1.023 0-1.637 0.987-0.614 0.988-0.62 2.957 0 1.33 0.276 2.222 0.28 0.886 0.789 1.332 0.508 0.44 1.193 0.44zM17.51 6q0.924 0 1.777 0.3 0.86 0.293 1.532 0.953 0.679 0.66 1.07 1.749 0.398 1.083 0.404 2.668-0.006 1.472-0.351 2.629-0.34 1.15-0.976 1.958-0.638 0.806-1.538 1.23-0.895 0.417-2.005 0.417-1.199 0-2.117-0.446-0.918-0.45-1.479-1.224-0.555-0.779-0.672-1.749h2.496q0.146 0.632 0.614 0.982 0.468 0.344 1.158 0.344 1.169 0 1.777-0.982 0.608-0.987 0.614-2.702h-0.082Q19.463 12.635 19.007 13q-0.456 0.362-1.046 0.559-0.585 0.197-1.246 0.197-1.058 0-1.894-0.48-0.83-0.479-1.31-1.32-0.479-0.846-0.473-1.929-0.006-1.173 0.555-2.082 0.562-0.914 1.567-1.433Q16.172 5.994 17.51 6zm0.018 1.918q-0.59 0-1.053 0.271Q16.02 8.46 15.75 8.923q-0.263 0.462-0.257 1.038-0.006 0.575 0.252 1.032 0.263 0.457 0.719 0.728 0.456 0.265 1.04 0.265 0.439 0 0.807-0.158 0.374-0.158 0.65-0.434 0.28-0.282 0.438-0.65 0.158-0.372 0.163-0.795-0.005-0.558-0.269-1.02-0.263-0.463-0.725-0.734-0.461-0.277-1.04-0.277z"
tools:ignore="VectorPath" />
<path
android:fillColor="@android:color/white"
android:pathData="M15.5 20.544h-7L12 24l3.5-3.456z" />
</vector>

View file

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6.542 18q-1.508-0.006-2.595-0.716-1.082-0.711-1.667-2.06-0.578-1.348-0.573-3.244 0-1.89 0.58-3.221 0.584-1.331 1.665-2.025 1.088-0.7 2.59-0.7 1.503 0 2.584 0.7 1.088 0.7 1.673 2.03 0.584 1.326 0.578 3.216 0 1.902-0.584 3.25-0.579 1.348-1.66 2.06Q8.05 18 6.542 18zm0-2.025q1.03 0 1.643-0.999 0.614-0.998 0.608-2.996 0-1.314-0.28-2.188-0.275-0.875-0.784-1.315-0.503-0.44-1.187-0.44-1.023 0-1.637 0.987-0.614 0.988-0.62 2.957 0 1.33 0.276 2.222 0.28 0.886 0.789 1.332 0.508 0.44 1.193 0.44zM17.51 6q0.924 0 1.777 0.3 0.86 0.293 1.532 0.953 0.679 0.66 1.07 1.749 0.398 1.083 0.404 2.668-0.006 1.472-0.351 2.629-0.34 1.15-0.976 1.958-0.638 0.806-1.538 1.23-0.895 0.417-2.005 0.417-1.199 0-2.117-0.446-0.918-0.45-1.479-1.224-0.555-0.779-0.672-1.749h2.496q0.146 0.632 0.614 0.982 0.468 0.344 1.158 0.344 1.169 0 1.777-0.982 0.608-0.987 0.614-2.702h-0.082Q19.463 12.635 19.007 13q-0.456 0.362-1.046 0.559-0.585 0.197-1.246 0.197-1.058 0-1.894-0.48-0.83-0.479-1.31-1.32-0.479-0.846-0.473-1.929-0.006-1.173 0.555-2.082 0.562-0.914 1.567-1.433Q16.172 5.994 17.51 6zm0.018 1.918q-0.59 0-1.053 0.271Q16.02 8.46 15.75 8.923q-0.263 0.462-0.257 1.038-0.006 0.575 0.252 1.032 0.263 0.457 0.719 0.728 0.456 0.265 1.04 0.265 0.439 0 0.807-0.158 0.374-0.158 0.65-0.434 0.28-0.282 0.438-0.65 0.158-0.372 0.163-0.795-0.005-0.558-0.269-1.02-0.263-0.463-0.725-0.734-0.461-0.277-1.04-0.277z"
tools:ignore="VectorPath" />
<path
android:fillColor="@android:color/white"
android:pathData="M15.5 3.456h-7L12 0l3.5 3.456z" />
</vector>

View file

@ -7,7 +7,7 @@
<shape android:shape="rectangle">
<stroke
android:width="@dimen/size_stroke_small"
android:color="@color/overlay_divider" />
android:color="@color/mtrl_btn_stroke_color_selector" />
</shape>
</item>
</layer-list>

View file

@ -1,137 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="album"
type="org.oxycblt.auxio.music.Album" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="@+id/album_cover"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge_land"
android:layout_height="@dimen/size_cover_huge_land"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_album_cover(album.name)}"
app:albumArt="@{album}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_album" />
<TextView
android:id="@+id/album_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{album.name}"
app:layout_constraintBottom_toTopOf="@+id/album_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toTopOf="@+id/album_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Album Name" />
<TextView
android:id="@+id/album_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> detailModel.navToItem(album.artist)}"
android:text="@{album.artist.name}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/album_details"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toBottomOf="@+id/album_name"
tools:text="Artist Name" />
<TextView
android:id="@+id/album_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:albumDetails="@{album}"
app:layout_constraintBottom_toBottomOf="@+id/album_cover"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toBottomOf="@+id/album_artist"
tools:text="2020 / 10 Songs / 16:16" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:onClick="@{() -> playbackModel.playAlbum(album, false)}"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/album_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_cover" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playAlbum(album, true)}"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/album_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_play_button"
app:layout_constraintTop_toTopOf="@+id/album_play_button" />
<TextView
android:id="@+id/album_song_header"
style="@style/Widget.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:background="@drawable/ui_header_dividers"
android:text="@string/lbl_songs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_play_button" />
<ImageButton
android:id="@+id/album_sort_button"
style="@style/Widget.Button.Unbounded.Small"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementAlbumSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/album_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/album_song_header"
app:sortIcon="@{detailModel.albumSortMode}"
tools:src="@drawable/ic_sort_numeric_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
<data>
<variable
name="artist"
type="org.oxycblt.auxio.music.Artist" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/artist_image"
android:layout_width="@dimen/size_cover_huge_land"
android:layout_height="@dimen/size_cover_huge_land"
android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_artist_image(artist.name)}"
style="@style/Widget.ImageView.Full"
app:artistImage="@{artist}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist" />
<TextView
android:id="@+id/artist_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{artist.name}"
app:layout_constraintBottom_toTopOf="@+id/artist_genre"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toTopOf="@+id/artist_image"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Artist Name" />
<TextView
android:id="@+id/artist_genre"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistGenre="@{artist}"
app:layout_constraintBottom_toTopOf="@+id/artist_counts"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toBottomOf="@+id/artist_name"
tools:text="Genre Name" />
<TextView
android:id="@+id/artist_counts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistCounts="@{artist}"
app:layout_constraintBottom_toBottomOf="@+id/artist_image"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toBottomOf="@+id/artist_genre"
tools:text="2 Albums, 20 Songs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:onClick="@{() -> playbackModel.playArtist(artist, false)}"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintEnd_toStartOf="@+id/artist_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_image" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playArtist(artist, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/artist_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/artist_play_button"
app:layout_constraintTop_toTopOf="@+id/artist_play_button" />
<TextView
android:id="@+id/artist_album_header"
android:layout_height="wrap_content"
android:layout_width="match_parent"
style="@style/Widget.TextView.Header"
android:layout_marginTop="@dimen/spacing_small"
android:text="@string/lbl_albums"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<!-- This layout is re-used across all detail fragments. Do not add databinding. -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
<ImageView
android:id="@+id/detail_cover"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge_land"
android:layout_height="@dimen/size_cover_huge_land"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toTopOf="@+id/detail_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Name" />
<TextView
android:id="@+id/detail_subhead"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="true"
android:focusable="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/detail_info"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_name"
tools:text="Info A" />
<TextView
android:id="@+id/detail_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/detail_cover"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_subhead"
tools:text="Info B" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_cover" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:layout_marginStart="@dimen/spacing_small"
android:backgroundTint="?attr/colorAccent"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,131 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_huge_land"
android:layout_height="@dimen/size_cover_huge_land"
android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_genre_image(genre.name)}"
style="@style/Widget.ImageView.Full"
app:genreImage="@{genre}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<TextView
android:id="@+id/genre_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{genre.resolvedName}"
app:layout_constraintBottom_toTopOf="@+id/genre_song_count"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toTopOf="@+id/genre_image"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{@plurals/fmt_song_count(genre.songs.size(), genre.songs.size())}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/genre_duration"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="2 Artists, 4 Albums" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:onClick="@{() -> playbackModel.playGenre(genre, false)}"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintEnd_toStartOf="@+id/genre_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playGenre(genre, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/genre_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/genre_play_button"
app:layout_constraintTop_toTopOf="@+id/genre_play_button" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/genre_image"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.TextView.Header"
android:layout_marginTop="@dimen/spacing_small"
android:text="@string/lbl_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_play_button" />
<ImageButton
android:id="@+id/genre_sort_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Button.Unbounded.Small"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
app:sortIcon="@{detailModel.genreSortMode}"
tools:src="@drawable/ic_sort_alpha_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,136 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="album"
type="org.oxycblt.auxio.music.Album" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="@+id/album_cover"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_album_cover(album.name)}"
style="@style/Widget.ImageView.Full"
app:albumArt="@{album}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_album" />
<TextView
android:id="@+id/album_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{album.name}"
app:layout_constraintBottom_toTopOf="@+id/album_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toTopOf="@+id/album_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Album Name" />
<TextView
android:id="@+id/album_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> detailModel.navToItem(album.artist)}"
android:text="@{album.artist.name}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/album_details"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toBottomOf="@+id/album_name"
tools:text="Artist Name" />
<TextView
android:id="@+id/album_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:albumDetails="@{album}"
app:layout_constraintBottom_toBottomOf="@+id/album_cover"
app:layout_constraintStart_toEndOf="@+id/album_cover"
app:layout_constraintTop_toBottomOf="@+id/album_artist"
tools:text="2020 / 10 Songs / 16:16" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:onClick="@{() -> playbackModel.playAlbum(album, false)}"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintEnd_toStartOf="@+id/album_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_cover" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playAlbum(album, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/album_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_play_button"
app:layout_constraintTop_toTopOf="@+id/album_play_button" />
<TextView
android:id="@+id/album_song_header"
style="@style/Widget.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:background="@drawable/ui_header_dividers"
android:text="@string/lbl_songs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_play_button" />
<ImageButton
android:id="@+id/album_sort_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Button.Unbounded.Small"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementAlbumSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/album_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/album_song_header"
app:sortIcon="@{detailModel.albumSortMode}"
tools:src="@drawable/ic_sort_numeric_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
<data>
<variable
name="artist"
type="org.oxycblt.auxio.music.Artist" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/artist_image"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_artist_image(artist.name)}"
style="@style/Widget.ImageView.Full"
app:artistImage="@{artist}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist" />
<TextView
android:id="@+id/artist_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{artist.name}"
app:layout_constraintBottom_toTopOf="@+id/artist_genre"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toTopOf="@+id/artist_image"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Artist Name" />
<TextView
android:id="@+id/artist_genre"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistGenre="@{artist}"
app:layout_constraintBottom_toTopOf="@+id/artist_counts"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toBottomOf="@+id/artist_name"
tools:text="Genre Name" />
<TextView
android:id="@+id/artist_counts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistCounts="@{artist}"
app:layout_constraintBottom_toBottomOf="@+id/artist_image"
app:layout_constraintStart_toEndOf="@+id/artist_image"
app:layout_constraintTop_toBottomOf="@+id/artist_genre"
tools:text="2 Albums, 20 Songs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:onClick="@{() -> playbackModel.playArtist(artist, false)}"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintEnd_toStartOf="@+id/artist_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_image" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playArtist(artist, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/artist_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/artist_play_button"
app:layout_constraintTop_toTopOf="@+id/artist_play_button" />
<TextView
android:id="@+id/artist_album_header"
android:layout_height="wrap_content"
android:layout_width="match_parent"
style="@style/Widget.TextView.Header"
android:layout_marginTop="@dimen/spacing_small"
android:text="@string/lbl_albums"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
<ImageView
android:id="@+id/detail_cover"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
style="@style/Widget.ImageView.Full"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toTopOf="@+id/detail_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Name" />
<TextView
android:id="@+id/detail_subhead"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="true"
android:focusable="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/detail_info"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_name"
tools:text="Info A" />
<TextView
android:id="@+id/detail_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/detail_cover"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_subhead"
tools:text="Info B" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_cover" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_genre_image(genre.name)}"
style="@style/Widget.ImageView.Full"
app:genreImage="@{genre}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<TextView
android:id="@+id/genre_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{genre.resolvedName}"
app:layout_constraintBottom_toTopOf="@+id/genre_song_count"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toTopOf="@+id/genre_image"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{@plurals/fmt_song_count(genre.songs.size(), genre.songs.size())}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/genre_duration"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="2 Artists, 4 Albums" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:onClick="@{() -> playbackModel.playGenre(genre, false)}"
android:text="@string/lbl_play"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_marginStart="@dimen/spacing_medium"
app:layout_constraintEnd_toStartOf="@+id/genre_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playGenre(genre, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/genre_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/genre_play_button"
app:layout_constraintTop_toTopOf="@+id/genre_play_button" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/genre_image"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.TextView.Header"
android:layout_marginTop="@dimen/spacing_small"
android:text="@string/lbl_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_play_button" />
<ImageButton
android:id="@+id/genre_sort_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Button.Unbounded.Small"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
app:sortIcon="@{detailModel.genreSortMode}"
tools:src="@drawable/ic_sort_alpha_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -29,6 +29,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:theme="@style/Theme.Neutral"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.google.android.material.card.MaterialCardView
@ -38,7 +39,7 @@
app:cardBackgroundColor="?attr/colorSurface"
app:cardCornerRadius="0dp"
app:cardElevation="0dp"
app:strokeColor="@color/overlay_divider"
app:strokeColor="@color/mtrl_btn_stroke_color_selector"
app:strokeWidth="1dp">
<androidx.constraintlayout.widget.ConstraintLayout
@ -158,7 +159,6 @@
<TextView
android:id="@+id/about_song_count"
style="@style/Widget.TextView.Icon"
android:theme="@style/Theme.Neutral"
app:drawableStartCompat="@drawable/ic_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -169,7 +169,7 @@
android:id="@+id/about_author"
style="@style/Widget.TextView.Icon"
android:text="@string/lbl_author"
app:drawableStartCompat="@drawable/ic_author"
app:drawableStartCompat="@drawable/ic_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/about_song_count" />

View file

@ -53,7 +53,7 @@
style="@style/Widget.TextView.Compact.Secondary"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@{@string/format_info(song.album.artist.name, song.album.name)}"
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -28,8 +28,7 @@
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_artist_header"
tools:layout_marginTop="56dp"/>
tools:listitem="@layout/item_detail" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -4,12 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.HeaderViewHolder">
<data>
<variable
name="header"
type="org.oxycblt.auxio.music.Header" />
type="org.oxycblt.auxio.music.ActionHeader" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -32,10 +31,12 @@
style="@style/Widget.Button.Unbounded.Small"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@{context.getString(header.desc)}"
android:src="@{context.getDrawable(header.icon)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_clear" />
tools:src="@drawable/ic_sort"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -37,7 +37,7 @@
<TextView
android:id="@+id/album_info"
style="@style/Widget.TextView.Item.Secondary"
app:albumInfo="@{album}"
android:text="@{@string/fmt_two(album.artist.name, @plurals/fmt_song_count(album.songs.size, album.songs.size))}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -1,128 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="album"
type="org.oxycblt.auxio.music.Album" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="@+id/album_cover"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginTop="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_album_cover(album.name)}"
app:albumArt="@{album}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_album" />
<TextView
android:id="@+id/album_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{album.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_cover"
tools:text="Album Name" />
<TextView
android:id="@+id/album_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:onClick="@{() -> detailModel.navToItem(album.artist)}"
android:text="@{album.artist.name}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_name"
tools:text="Artist Name" />
<TextView
android:id="@+id/album_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:albumDetails="@{album}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_artist"
tools:text="2020 / 10 Songs / 16:16" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:onClick="@{() -> playbackModel.playAlbum(album, false)}"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/album_shuffle_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/album_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playAlbum(album, true)}"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/album_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_play_button"
app:layout_constraintTop_toTopOf="@+id/album_play_button" />
<TextView
android:id="@+id/album_song_header"
style="@style/Widget.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:background="@drawable/ui_header_dividers"
android:text="@string/lbl_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_play_button" />
<ImageButton
android:id="@+id/album_sort_button"
style="@style/Widget.Button.Unbounded.Small"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementAlbumSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/album_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/album_song_header"
app:sortIcon="@{detailModel.albumSortMode}"
tools:src="@drawable/ic_sort_numeric_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

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

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistAlbumViewHolder">
tools:context=".detail.recycler.ArtistDetailAdapter.ArtistAlbumViewHolder">
<data>
@ -38,7 +38,7 @@
<TextView
android:id="@+id/album_year"
style="@style/Widget.TextView.Item.Secondary"
app:albumYear="@{album}"
android:text="@{album.year != 0 ? String.valueOf(album.year) : @string/def_date}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
<data>
<variable
name="artist"
type="org.oxycblt.auxio.music.Artist" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/artist_image"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginTop="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_artist_image(artist.name)}"
app:artistImage="@{artist}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist" />
<TextView
android:id="@+id/artist_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{artist.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_image"
tools:text="Artist Name" />
<TextView
android:id="@+id/artist_genre"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistGenre="@{artist}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_name"
tools:text="Genre Name" />
<TextView
android:id="@+id/artist_counts"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:artistCounts="@{artist}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_genre"
tools:text="2 Albums, 20 Songs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:onClick="@{() -> playbackModel.playArtist(artist, false)}"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/artist_shuffle_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_counts" />
<com.google.android.material.button.MaterialButton
android:id="@+id/artist_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_medium"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playArtist(artist, true)}"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/artist_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/artist_play_button"
app:layout_constraintTop_toTopOf="@+id/artist_play_button" />
<TextView
android:id="@+id/artist_song_header"
style="@style/Widget.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:background="@drawable/ui_header_dividers"
android:text="@string/lbl_albums"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/artist_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<!-- This layout is re-used across all detail fragments. Do not add databinding. -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
<ImageView
android:id="@+id/detail_cover"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_artist"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_cover"
tools:text="Name" />
<TextView
android:id="@+id/detail_subhead"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_name"
tools:text="Info A" />
<TextView
android:id="@+id/detail_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_subhead"
tools:text="Info B" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_info" />
<com.google.android.material.button.MaterialButton
android:id="@+id/detail_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:layout_marginStart="@dimen/spacing_small"
android:backgroundTint="?attr/colorAccent"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/genre_image"
style="@style/Widget.ImageView.Full"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginTop="@dimen/spacing_medium"
android:contentDescription="@{@string/desc_genre_image(genre.name)}"
app:genreImage="@{genre}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/genre_name"
style="@style/Widget.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{genre.resolvedName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{@plurals/fmt_song_count(genre.songs.size(), genre.songs.size())}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="80 Songs" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_play_button"
style="@style/Widget.Button.Vibrant.Secondary"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small"
android:onClick="@{() -> playbackModel.playGenre(genre, false)}"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/genre_shuffle_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_duration" />
<com.google.android.material.button.MaterialButton
android:id="@+id/genre_shuffle_button"
style="@style/Widget.Button.Vibrant.Primary"
android:backgroundTint="?attr/colorAccent"
android:onClick="@{() -> playbackModel.playGenre(genre, true)}"
android:text="@string/lbl_shuffle"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="@+id/genre_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/genre_play_button"
app:layout_constraintTop_toTopOf="@+id/genre_play_button" />
<TextView
android:id="@+id/genre_song_header"
style="@style/Widget.TextView.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:text="@string/lbl_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_play_button" />
<ImageButton
android:id="@+id/genre_sort_button"
style="@style/Widget.Button.Unbounded.Small"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
app:sortIcon="@{detailModel.genreSortMode}"
tools:src="@drawable/ic_sort_alpha_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreSongViewHolder">
tools:context=".detail.recycler.GenreDetailAdapter.GenreSongViewHolder">
<data>
@ -40,7 +40,7 @@
android:id="@+id/song_info"
style="@style/Widget.TextView.Item.Secondary"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/format_info(song.album.artist.name, song.album.name)}"
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_duration"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -43,7 +43,7 @@
android:id="@+id/song_info"
style="@style/Widget.TextView.Item.Secondary"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/format_info(song.album.artist.name, song.album.name)}"
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_drag_handle"
app:layout_constraintStart_toEndOf="@+id/album_cover"
@ -52,13 +52,14 @@
<ImageView
android:id="@+id/song_drag_handle"
android:layout_width="@dimen/size_btn_small"
android:layout_width="wrap_content"
android:layout_height="@dimen/size_btn_small"
android:layout_marginEnd="@dimen/spacing_small"
android:clickable="true"
android:contentDescription="@string/desc_queue_handle"
android:focusable="true"
android:scaleType="center"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:src="@drawable/ic_handle"
app:layout_constraintBottom_toBottomOf="@+id/album_cover"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -40,7 +40,7 @@
android:id="@+id/song_info"
style="@style/Widget.TextView.Item.Secondary"
android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/format_info(song.album.artist.name, song.album.name)}"
android:text="@{@string/fmt_two(song.album.artist.name, song.album.name)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
<item
android:id="@+id/option_sort_dsc"
android:title="@string/lbl_sort_dsc" />
<item
android:id="@+id/option_sort_artist"
android:title="@string/lbl_sort_artist" />
<item
android:id="@+id/option_sort_album"
android:title="@string/lbl_sort_album" />
<item
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
</group>
</menu>

View file

@ -10,7 +10,7 @@
<item
android:id="@+id/submenu_sorting"
android:icon="@drawable/ic_sort_none"
android:icon="@drawable/ic_sort"
android:title="@string/lbl_sort"
app:showAsAction="ifRoom">
<menu>

View file

@ -100,7 +100,7 @@
<string name="hint_search_library">"Prohledat vaši knihovnu…"</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="desc_sort_button">"Změnit pořadí řazení"</string>
<string name="desc_sort">"Změnit pořadí řazení"</string>
<string name="desc_track_number">"Stopa %d"</string>
<string name="desc_play_pause">"Přehrát nebo pozastavit"</string>
<string name="desc_skip_next">"Přeskočit na další skladbu"</string>

View file

@ -103,7 +103,7 @@
<string name="hint_search_library">Musikbibliothek durchsuchen…</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="desc_sort_button">Reihenfolge ändern</string>
<string name="desc_sort">Reihenfolge ändern</string>
<string name="desc_track_number">Titel %d</string>
<string name="desc_play_pause">Abspielen oder Pausieren</string>

View file

@ -106,7 +106,7 @@
<string name="hint_search_library">Busca en tu biblioteca…</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="desc_sort_button">Cambiar el orden de clasificación</string>
<string name="desc_sort">Cambiar el orden de clasificación</string>
<string name="desc_track_number">Pista %d</string>
<string name="desc_play_pause">Reproducir o Pausar</string>

View file

@ -104,7 +104,7 @@
<string name="hint_search_library">Zoek in uw bibliotheek…</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="desc_sort_button">Sorteervolgorde wijzigen</string>
<string name="desc_sort">Sorteervolgorde wijzigen</string>
<string name="desc_track_number">Nummer %d</string>
<string name="desc_play_pause">Afspelen/Pauzeren</string>

View file

@ -14,8 +14,8 @@
<dimen name="size_cover_compact">48dp</dimen>
<dimen name="size_cover_normal">56dp</dimen>
<dimen name="size_cover_huge_land">136dp</dimen>
<dimen name="size_cover_huge">264dp</dimen>
<dimen name="size_cover_huge_land">128dp</dimen>
<dimen name="size_cover_huge">256dp</dimen>
<dimen name="size_stroke_small">1dp</dimen>
<dimen name="size_stroke_large">2dp</dimen>

View file

@ -4,8 +4,8 @@
<string name="info_app_name" translatable="false">Auxio</string>
<!-- Format Namespace | Value formatting/plurals -->
<string name="format_info" translatable="false">%1$s • %2$s</string>
<string name="format_double_info" translatable="false">%1$s • %2$s • %3$s</string>
<string name="format_double_counts" translatable="false">%1$s, %2$s</string>
<string name="format_accent_summary" translatable="false">&lt;b>%1$s&lt;/b>:&#160;%2$s</string>
<string name="fmt_two" translatable="false">%1$s • %2$s</string>
<string name="fmt_three" translatable="false">%1$s • %2$s • %3$s</string>
<string name="fmt_counts" translatable="false">%1$s, %2$s</string>
<string name="fmt_accent_desc" translatable="false">&lt;b>%1$s&lt;/b>:&#160;%2$s</string>
</resources>

View file

@ -114,7 +114,6 @@
<string name="hint_search_library">Search your library…</string>
<!-- Description Namespace | Accessibility Strings -->
<string name="desc_sort_button">Change Sort Order</string>
<string name="desc_track_number">Track %d</string>
<string name="desc_play_pause">Play or Pause</string>

View file

@ -11,7 +11,7 @@
<style name="Theme.Base" parent="Theme.Splash">
<!-- Colors -->
<item name="colorSurface">@color/surface</item>
<item name="colorAccent">@color/design_default_color_primary</item>
<item name="colorAccent">@color/blue</item>
<item name="colorPrimary">?attr/colorAccent</item>
<item name="colorOnPrimary">?attr/colorSurface</item>

View file

@ -197,26 +197,24 @@
<item name="android:background">@drawable/ui_small_unbounded_ripple</item>
</style>
<style name="Widget.Button.Vibrant.Base" parent="@style/Widget.MaterialComponents.Button.TextButton">
<style name="Widget.Button.Vibrant.Primary" parent="@style/Widget.MaterialComponents.Button.TextButton">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:letterSpacing">0.05</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/text_size_small</item>
<item name="textAllCaps">false</item>
<item name="android:textColor">?attr/colorSurface</item>
<item name="fontFamily">@font/inter_semibold</item>
<item name="cornerRadius">0dp</item>
</style>
<style name="Widget.Button.Vibrant.Primary" parent="@style/Widget.Button.Vibrant.Base">
<item name="android:textColor">?attr/colorSurface</item>
<item name="backgroundTint">?attr/colorAccent</item>
<item name="rippleColor">@color/mtrl_btn_ripple_color</item>
</style>
<style name="Widget.Button.Vibrant.Secondary" parent="@style/Widget.Button.Vibrant.Base">
<item name="strokeColor">@color/overlay_divider</item>
<item name="strokeWidth">@dimen/size_stroke_small</item>
<item name="rippleColor">?attr/colorControlHighlight</item>
<style name="Widget.Button.Vibrant.Secondary" parent="@style/Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:letterSpacing">0.05</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/text_size_small</item>
<item name="fontFamily">@font/inter_semibold</item>
<item name="cornerRadius">0dp</item>
</style>
</resources>