music: add picker dialog

Add a dialog for picking a genre from several choices.

This basically completes multi-genre support in Auxio, save more
internal reworks.

Note that it is extremely likely that the "Play from genre" setting
will be removed soon. This feature has made me realize that such does
not many any real sense, as genres are more semantically similar to
playlists than artists or albums. This implementation only exists to
make multi-artist support an easy plug-and-play operation.

Resolves #201
This commit is contained in:
Alexander Capehart 2022-09-14 19:48:39 -06:00
parent 2aa540c29a
commit 717f49fc20
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
37 changed files with 451 additions and 106 deletions

View file

@ -29,16 +29,20 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearSmoothScroller
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.canScroll
@ -125,11 +129,19 @@ class AlbumDetailFragment :
override fun onItemClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
val playbackMode = settings.detailPlaybackMode
if (playbackMode != null) {
playbackModel.play(item, playbackMode)
} else {
playbackModel.playFromAlbum(item)
when (settings.detailPlaybackMode) {
null, MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.showGenrePickerDialog(item.uid, PickerMode.PLAY)
)
)
} else {
playbackModel.playFromGenre(item, item.genres[0])
}
}
}

View file

@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
@ -33,10 +34,13 @@ import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect
@ -117,11 +121,19 @@ class ArtistDetailFragment :
override fun onItemClick(item: Item) {
when (item) {
is Song -> {
val playbackMode = settings.detailPlaybackMode
if (playbackMode != null) {
playbackModel.play(item, playbackMode)
} else {
playbackModel.playFromArtist(item)
when (settings.detailPlaybackMode) {
null, MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.showGenrePickerDialog(item.uid, PickerMode.PLAY)
)
)
} else {
playbackModel.playFromGenre(item, item.genres[0])
}
}
}
is Album -> navModel.exploreNavigateTo(item)

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.ReleaseType
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item

View file

@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter
@ -34,10 +35,13 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect
@ -117,11 +121,20 @@ class GenreDetailFragment :
override fun onItemClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
val playbackMode = settings.detailPlaybackMode
if (playbackMode != null) {
playbackModel.play(item, playbackMode)
} else {
playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value))
when (settings.detailPlaybackMode) {
null -> playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value))
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.showGenrePickerDialog(item.uid, PickerMode.PLAY)
)
)
} else {
playbackModel.playFromGenre(item, item.genres[0])
}
}
}

View file

@ -21,7 +21,7 @@ import android.content.Context
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logD
/**

View file

@ -48,11 +48,11 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel

View file

@ -26,10 +26,10 @@ import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD

View file

@ -24,11 +24,11 @@ import android.view.ViewGroup
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.secsToMs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item

View file

@ -23,10 +23,10 @@ import android.view.ViewGroup
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item

View file

@ -23,10 +23,10 @@ import android.view.ViewGroup
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item

View file

@ -21,15 +21,18 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.music.secsToMs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -109,7 +112,20 @@ class SongListFragment : HomeListFragment<Song>() {
override fun onItemClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.java}" }
playbackModel.play(item, settings.libPlaybackMode)
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.showGenrePickerDialog(item.uid, PickerMode.PLAY)
)
)
} else {
playbackModel.playFromGenre(item, item.genres[0])
}
}
}
override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -21,7 +21,7 @@ import org.oxycblt.auxio.home.tabs.Tab.Companion.fromSequence
import org.oxycblt.auxio.home.tabs.Tab.Companion.toSequence
import org.oxycblt.auxio.home.tabs.Tab.Invisible
import org.oxycblt.auxio.home.tabs.Tab.Visible
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logE
/**

View file

@ -24,7 +24,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.util.inflater

View file

@ -25,7 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import kotlin.math.min
/** A basic keyer for music data. */

View file

@ -26,8 +26,6 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Date.Companion.from
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.nonZeroOrNull
@ -105,43 +103,34 @@ sealed class Music : Item {
*/
@Parcelize
class UID private constructor(
private val isMusicBrainz: Boolean,
private val format: Format,
private val mode: MusicMode,
private val uuid: UUID
) : Parcelable {
// Cache the hashCode for speed
@IgnoredOnParcel private val hashCode: Int
@IgnoredOnParcel private var hashCode = format.hashCode()
init {
var result = isMusicBrainz.hashCode()
result = 31 * result + mode.hashCode()
result = 31 * result + uuid.hashCode()
hashCode = result
hashCode = 31 * hashCode + mode.hashCode()
hashCode = 31 * hashCode + uuid.hashCode()
}
override fun hashCode() = hashCode
override fun equals(other: Any?) = other is UID &&
isMusicBrainz == other.isMusicBrainz &&
format == other.format &&
mode == other.mode && uuid == other.uuid
override fun toString(): String {
// Format comes first, delimited by a ":".
val format = if (isMusicBrainz) {
FORMAT_MUSICBRAINZ
} else {
FORMAT_AUXIO
}
// UID string format is roughly:
// format_namespace:music_mode_int-uuid
override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid"
// Instead of making new string values for the mode, be lazy and just append it's
// intCode in front of the UUID.
return "$format:${mode.intCode.toString(16)}-$uuid"
private enum class Format(val namespace: String) {
AUXIO("org.oxycblt.auxio"),
MUSICBRAINZ("org.musicbrainz")
}
companion object {
private const val FORMAT_AUXIO = "org.oxycblt.auxio"
private const val FORMAT_MUSICBRAINZ = "org.musicbrainz"
/** Parse a [UID] from the string [uid]. Returns null if not valid. */
fun fromString(uid: String): UID? {
val split = uid.split(':', limit = 2)
@ -149,9 +138,9 @@ sealed class Music : Item {
return null
}
val isMusicBrainz = when (split[0]) {
FORMAT_MUSICBRAINZ -> true
FORMAT_AUXIO -> false
val format = when (split[0]) {
Format.AUXIO.namespace -> Format.AUXIO
Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ
else -> return null
}
@ -163,7 +152,7 @@ sealed class Music : Item {
val mode = MusicMode.fromInt(ids[0].toIntOrNull(16) ?: return null) ?: return null
val uuid = UUID.fromString(ids[1])
return UID(isMusicBrainz, mode, uuid)
return UID(format, mode, uuid)
}
/**
@ -178,7 +167,7 @@ sealed class Music : Item {
val digest = MessageDigest.getInstance("MD5")
updates(digest)
val uuid = digest.digest().toUuid()
return UID(false, mode, uuid)
return UID(Format.AUXIO, mode, uuid)
}
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.ui
package org.oxycblt.auxio.music
import org.oxycblt.auxio.IntegerTable

View file

@ -26,7 +26,6 @@ import android.provider.MediaStore
import android.text.format.DateUtils
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD
import java.util.UUID
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor(

View file

@ -15,18 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.ui
package org.oxycblt.auxio.music
import androidx.annotation.IdRes
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.Sort.Mode
import org.oxycblt.auxio.music.Sort.Mode
/**
* Represents the sort modes used in Auxio.

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.settings
package org.oxycblt.auxio.music.dirs
import android.view.View
import android.view.ViewGroup

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.settings
package org.oxycblt.auxio.music.dirs
import org.oxycblt.auxio.music.Directory

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.settings
package org.oxycblt.auxio.music.dirs
import android.net.Uri
import android.os.Bundle

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.ui.recycler.ItemClickListener
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/**
* The adapter that displays a list of genre choices in the picker UI.
*/
class GenreChoiceAdapter(private val listener: ItemClickListener) : RecyclerView.Adapter<GenreChoiceViewHolder>() {
private var genres = listOf<Genre>()
override fun getItemCount() = genres.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreChoiceViewHolder.new(parent)
override fun onBindViewHolder(holder: GenreChoiceViewHolder, position: Int) =
holder.bind(genres[position], listener)
fun submitList(newGenres: List<Genre>) {
if (newGenres != genres) {
genres = newGenres
@Suppress("NotifyDataSetChanged")
notifyDataSetChanged()
}
}
}
/**
* The ViewHolder that displays a genre choice. Smaller than other parent items due to dialog
* constraints.
*/
class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogViewHolder(binding.root) {
fun bind(genre: Genre, listener: ItemClickListener) {
binding.pickerImage.bind(genre)
binding.pickerName.text = genre.resolveName(binding.context)
binding.root.setOnClickListener {
listener.onItemClick(genre)
}
}
companion object {
fun new(parent: View) =
GenreChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater))
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.ItemClickListener
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A dialog that shows several genre options if the result of an genre-reliant operation is
* ambiguous.
* @author OxygenCobalt
*/
class GenrePickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
private val pickerModel: PickerViewModel by viewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val args: GenrePickerDialogArgs by navArgs()
private val adapter = GenreChoiceAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder
.setTitle(
when (args.pickerMode) {
PickerMode.GO -> R.string.lbl_go_genre
PickerMode.PLAY -> R.string.lbl_play_genre
}
)
.setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
pickerModel.setSongUid(args.songUid)
binding.pickerRecycler.adapter = adapter
collectImmediately(pickerModel.currentSong) { song ->
if (song != null) {
adapter.submitList(song.genres)
} else {
findNavController().navigateUp()
}
}
}
override fun onDestroyBinding(binding: DialogMusicPickerBinding) {
binding.pickerRecycler.adapter = null
}
override fun onItemClick(item: Item) {
check(item is Genre) { "Unexpected datatype: ${item::class.simpleName}" }
findNavController().navigateUp()
when (args.pickerMode) {
PickerMode.GO -> navModel.exploreNavigateTo(item)
PickerMode.PLAY -> {
val song = unlikelyToBeNull(pickerModel.currentSong.value)
playbackModel.playFromGenre(song, item)
}
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
/**
* Represents the actions available to the picker UI.
*/
enum class PickerMode {
PLAY,
GO
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A small ViewModel holding and updating the current song being shown in the picker UI.
* @author OxygenCobalt
*/
class PickerViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
private val _currentSong = MutableStateFlow<Song?>(null)
val currentSong: StateFlow<Song?> get() = _currentSong
fun setSongUid(uid: Music.UID) {
if (_currentSong.value?.uid == uid) return
val library = unlikelyToBeNull(musicStore.library)
_currentSong.value = requireNotNull(library.find(uid)) { "Invalid song id provided" }
}
override fun onLibraryChanged(library: MusicStore.Library?) {
if (library != null) {
val song = _currentSong.value
if (song != null) {
_currentSong.value = library.sanitize(song)
}
}
}
}

View file

@ -15,14 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.settings
package org.oxycblt.auxio.music.separators
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.view.children
import com.google.android.material.checkbox.MaterialCheckBox
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings

View file

@ -32,12 +32,12 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer
import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer
import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
import org.oxycblt.auxio.music.extractor.CacheLayer
import org.oxycblt.auxio.music.extractor.MetadataLayer
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW

View file

@ -32,7 +32,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.dsToMs
import org.oxycblt.auxio.music.msToDs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -92,19 +91,9 @@ class PlaybackViewModel(application: Application) :
// --- PLAYING FUNCTIONS ---
/** Play a [song] with the [mode] specified, */
fun play(song: Song, mode: MusicMode) {
// TODO: Remove this function when selection is implemented
val parent =
when (mode) {
MusicMode.GENRES -> song.album
MusicMode.ARTISTS -> song.album.artist
MusicMode.ALBUMS -> song.genres.maxBy { it.songs.size }
MusicMode.SONGS -> null
}
playbackManager.play(song, parent, settings)
/** Play a [song] from all songs. */
fun play(song: Song) {
playbackManager.play(song, null, settings)
}
/** Play a song from it's album. */

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings

View file

@ -29,16 +29,19 @@ import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -147,7 +150,20 @@ class SearchFragment :
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.play(item, settings.libPlaybackMode)
is Song -> when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.play(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> if (item.genres.size > 1) {
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.showGenrePickerDialog(item.uid, PickerMode.PLAY)
)
)
} else {
playbackModel.playFromGenre(item, item.genres[0])
}
}
is MusicParent -> navModel.exploreNavigateTo(item)
}
}

View file

@ -33,10 +33,10 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item

View file

@ -28,9 +28,9 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.settings.MusicDirs
import org.oxycblt.auxio.music.ui.MusicMode
import org.oxycblt.auxio.music.ui.Sort
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.dirs.MusicDirs
import org.oxycblt.auxio.playback.BarAction
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView 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"
android:id="@+id/picker_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_picker_choice" />

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="@dimen/spacing_mid_large"
android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_mid_large"
android:paddingBottom="@dimen/spacing_medium">
<org.oxycblt.auxio.image.ImageGroup
android:id="@+id/picker_image"
style="@style/Widget.Auxio.Image.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_song_24" />
<TextView
android:id="@+id/picker_name"
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:textColor="@color/sel_accented_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/picker_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Artist" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -17,8 +17,24 @@
<action
android:id="@+id/action_show_details"
app:destination="@id/song_detail_dialog" />
<action
android:id="@+id/show_genre_picker_dialog"
app:destination="@id/genre_picker_dialog" />
</fragment>
<dialog
android:id="@+id/genre_picker_dialog"
android:name="org.oxycblt.auxio.music.picker.GenrePickerDialog"
android:label="genre_picker_dialog"
tools:layout="@layout/dialog_music_picker">
<argument
android:name="songUid"
app:argType="org.oxycblt.auxio.music.Music$UID" />
<argument
android:name="pickerMode"
app:argType="org.oxycblt.auxio.music.picker.PickerMode" />
</dialog>
<dialog
android:id="@+id/song_detail_dialog"
android:name="org.oxycblt.auxio.detail.SongDetailDialog"
@ -67,12 +83,12 @@
tools:layout="@layout/dialog_pre_amp" />
<dialog
android:id="@+id/music_dirs_dialog"
android:name="org.oxycblt.auxio.music.settings.MusicDirsDialog"
android:name="org.oxycblt.auxio.music.dirs.MusicDirsDialog"
android:label="music_dirs_dialog"
tools:layout="@layout/dialog_music_dirs" />
<dialog
android:id="@+id/separators_dialog"
android:name="org.oxycblt.auxio.music.settings.SeparatorsDialog"
android:name="org.oxycblt.auxio.music.separators.SeparatorsDialog"
android:label="music_dirs_dialog"
tools:layout="@layout/dialog_separators" />

View file

@ -102,8 +102,11 @@
<string name="lbl_play_next">Play next</string>
<string name="lbl_queue_add">Add to queue</string>
<string name="lbl_go_genre">Go to genre</string>
<string name="lbl_go_artist">Go to artist</string>
<string name="lbl_go_album">Go to album</string>
<string name="lbl_play_genre">Play from genre</string>
<string name="lbl_play_artist">Play from artist</string>
<string name="lbl_song_detail">View properties</string>
<string name="lbl_props">Song properties</string>
@ -203,8 +206,8 @@
<string name="set_playback_mode_none">Play from shown item</string>
<string name="set_playback_mode_all">Play from all songs</string>
<string name="set_playback_mode_album">Play from album</string>
<string name="set_playback_mode_artist">Play from artist</string>
<string name="set_playback_mode_genre">Play from genre</string>
<string name="set_playback_mode_artist">@string/lbl_play_artist</string>
<string name="set_playback_mode_genre">@string/lbl_play_genre</string>
<string name="set_keep_shuffle">Remember shuffle</string>
<string name="set_keep_shuffle_desc">Keep shuffle on when playing a new song</string>
<string name="set_rewind_prev">Rewind before skipping back</string>