Merge branch 'dev' into feature/share
This commit is contained in:
commit
41cab82523
39 changed files with 484 additions and 229 deletions
|
@ -12,6 +12,9 @@ be parsed as images
|
||||||
- Fixed issue where searches would match song file names case-sensitively
|
- Fixed issue where searches would match song file names case-sensitively
|
||||||
- Fixed issue where the notification would not respond to changes in the album cover setting
|
- Fixed issue where the notification would not respond to changes in the album cover setting
|
||||||
- Fixed issue where short names starting with an article would not be correctly sorted (ex. "the 1")
|
- Fixed issue where short names starting with an article would not be correctly sorted (ex. "the 1")
|
||||||
|
- Fixed incorrect item arrangement on landscape
|
||||||
|
- Fixed disappearing dividers in search view
|
||||||
|
- Reduced likelihood that images (eg. album covers) would not update when the music library changed
|
||||||
|
|
||||||
#### Dev/Meta
|
#### Dev/Meta
|
||||||
- Switched to androidx media3 (New Home of ExoPlayer) for backing player components
|
- Switched to androidx media3 (New Home of ExoPlayer) for backing player components
|
||||||
|
|
|
@ -37,16 +37,18 @@ object IntegerTable {
|
||||||
const val VIEW_TYPE_PLAYLIST = 0xA004
|
const val VIEW_TYPE_PLAYLIST = 0xA004
|
||||||
/** BasicHeaderViewHolder */
|
/** BasicHeaderViewHolder */
|
||||||
const val VIEW_TYPE_BASIC_HEADER = 0xA005
|
const val VIEW_TYPE_BASIC_HEADER = 0xA005
|
||||||
|
/** DividerViewHolder */
|
||||||
|
const val VIEW_TYPE_DIVIDER = 0xA006
|
||||||
/** SortHeaderViewHolder */
|
/** SortHeaderViewHolder */
|
||||||
const val VIEW_TYPE_SORT_HEADER = 0xA006
|
const val VIEW_TYPE_SORT_HEADER = 0xA007
|
||||||
/** AlbumSongViewHolder */
|
/** AlbumSongViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
const val VIEW_TYPE_ALBUM_SONG = 0xA008
|
||||||
/** ArtistAlbumViewHolder */
|
/** ArtistAlbumViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA008
|
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
||||||
/** ArtistSongViewHolder */
|
/** ArtistSongViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_SONG = 0xA009
|
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
||||||
/** DiscHeaderViewHolder */
|
/** DiscHeaderViewHolder */
|
||||||
const val VIEW_TYPE_DISC_HEADER = 0xA00A
|
const val VIEW_TYPE_DISC_HEADER = 0xA00B
|
||||||
/** "Music playback" notification code */
|
/** "Music playback" notification code */
|
||||||
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
|
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
|
||||||
/** "Music loading" notification code */
|
/** "Music loading" notification code */
|
||||||
|
|
|
@ -136,8 +136,9 @@ class MainFragment :
|
||||||
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
||||||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||||
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
||||||
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
collect(musicModel.playlistToRename.flow, ::handleRenamePlaylist)
|
||||||
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
|
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
|
||||||
|
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||||
|
@ -315,12 +316,11 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAddToPlaylist(songs: List<Song>?) {
|
private fun handleRenamePlaylist(playlist: Playlist?) {
|
||||||
if (songs != null) {
|
if (playlist != null) {
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigateSafe(
|
.navigateSafe(MainFragmentDirections.actionRenamePlaylist(playlist.uid))
|
||||||
MainFragmentDirections.actionAddToPlaylist(songs.map { it.uid }.toTypedArray()))
|
musicModel.playlistToRename.consume()
|
||||||
musicModel.songsToAdd.consume()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,6 +331,16 @@ class MainFragment :
|
||||||
musicModel.playlistToDelete.consume()
|
musicModel.playlistToDelete.consume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleAddToPlaylist(songs: List<Song>?) {
|
||||||
|
if (songs != null) {
|
||||||
|
findNavController()
|
||||||
|
.navigateSafe(
|
||||||
|
MainFragmentDirections.actionAddToPlaylist(songs.map { it.uid }.toTypedArray()))
|
||||||
|
musicModel.songsToAdd.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePlaybackArtistPicker(song: Song?) {
|
private fun handlePlaybackArtistPicker(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -34,6 +35,8 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter
|
import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter
|
import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter
|
||||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -45,6 +48,7 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
@ -95,7 +99,17 @@ class AlbumDetailFragment :
|
||||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
binding.detailRecycler.apply {
|
||||||
|
adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
||||||
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
|
if (it != 0) {
|
||||||
|
val item = detailModel.albumList.value[it - 1]
|
||||||
|
item is Divider || item is Header || item is Disc
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -34,6 +35,8 @@ import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter
|
import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter
|
||||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -94,7 +97,17 @@ class ArtistDetailFragment :
|
||||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter)
|
binding.detailRecycler.apply {
|
||||||
|
adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter)
|
||||||
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
|
if (it != 0) {
|
||||||
|
val item = detailModel.artistList.value[it - 1]
|
||||||
|
item is Divider || item is Header
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
|
|
@ -32,6 +32,7 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.list.SortHeader
|
import org.oxycblt.auxio.detail.list.SortHeader
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
|
@ -297,7 +298,9 @@ constructor(
|
||||||
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
private fun refreshAlbumList(album: Album, replace: Boolean = false) {
|
||||||
logD("Refreshing album list")
|
logD("Refreshing album list")
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
list.add(SortHeader(R.string.lbl_songs))
|
val header = SortHeader(R.string.lbl_songs)
|
||||||
|
list.add(Divider(header))
|
||||||
|
list.add(header)
|
||||||
val instructions =
|
val instructions =
|
||||||
if (replace) {
|
if (replace) {
|
||||||
// Intentional so that the header item isn't replaced with the songs
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
|
@ -355,7 +358,9 @@ constructor(
|
||||||
logD("Release groups for this artist: ${byReleaseGroup.keys}")
|
logD("Release groups for this artist: ${byReleaseGroup.keys}")
|
||||||
|
|
||||||
for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
|
for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
|
||||||
list.add(BasicHeader(entry.key.headerTitleRes))
|
val header = BasicHeader(entry.key.headerTitleRes)
|
||||||
|
list.add(Divider(header))
|
||||||
|
list.add(header)
|
||||||
list.addAll(entry.value)
|
list.addAll(entry.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +368,9 @@ constructor(
|
||||||
var instructions: UpdateInstructions = UpdateInstructions.Diff
|
var instructions: UpdateInstructions = UpdateInstructions.Diff
|
||||||
if (artist.songs.isNotEmpty()) {
|
if (artist.songs.isNotEmpty()) {
|
||||||
logD("Songs present in this artist, adding header")
|
logD("Songs present in this artist, adding header")
|
||||||
list.add(SortHeader(R.string.lbl_songs))
|
val header = SortHeader(R.string.lbl_songs)
|
||||||
|
list.add(Divider(header))
|
||||||
|
list.add(header)
|
||||||
if (replace) {
|
if (replace) {
|
||||||
// Intentional so that the header item isn't replaced with the songs
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
instructions = UpdateInstructions.Replace(list.size)
|
instructions = UpdateInstructions.Replace(list.size)
|
||||||
|
@ -379,9 +386,14 @@ constructor(
|
||||||
logD("Refreshing genre list")
|
logD("Refreshing genre list")
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
// Genre is guaranteed to always have artists and songs.
|
// Genre is guaranteed to always have artists and songs.
|
||||||
list.add(BasicHeader(R.string.lbl_artists))
|
val artistHeader = BasicHeader(R.string.lbl_artists)
|
||||||
|
list.add(Divider(artistHeader))
|
||||||
|
list.add(artistHeader)
|
||||||
list.addAll(genre.artists)
|
list.addAll(genre.artists)
|
||||||
list.add(SortHeader(R.string.lbl_songs))
|
|
||||||
|
val songHeader = SortHeader(R.string.lbl_songs)
|
||||||
|
list.add(Divider(songHeader))
|
||||||
|
list.add(songHeader)
|
||||||
val instructions =
|
val instructions =
|
||||||
if (replace) {
|
if (replace) {
|
||||||
// Intentional so that the header item isn't replaced with the songs
|
// Intentional so that the header item isn't replaced with the songs
|
||||||
|
@ -400,7 +412,9 @@ constructor(
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
|
||||||
if (playlist.songs.isNotEmpty()) {
|
if (playlist.songs.isNotEmpty()) {
|
||||||
list.add(SortHeader(R.string.lbl_songs))
|
val header = SortHeader(R.string.lbl_songs)
|
||||||
|
list.add(Divider(header))
|
||||||
|
list.add(header)
|
||||||
if (replace) {
|
if (replace) {
|
||||||
instructions = UpdateInstructions.Replace(list.size)
|
instructions = UpdateInstructions.Replace(list.size)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -34,6 +35,8 @@ import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter
|
import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||||
import org.oxycblt.auxio.detail.list.GenreDetailListAdapter
|
import org.oxycblt.auxio.detail.list.GenreDetailListAdapter
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -87,7 +90,17 @@ class GenreDetailFragment :
|
||||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter)
|
binding.detailRecycler.apply {
|
||||||
|
adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter)
|
||||||
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
|
if (it != 0) {
|
||||||
|
val item = detailModel.genreList.value[it - 1]
|
||||||
|
item is Divider || item is Header
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -34,6 +35,8 @@ import org.oxycblt.auxio.detail.header.DetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.header.PlaylistDetailHeaderAdapter
|
import org.oxycblt.auxio.detail.header.PlaylistDetailHeaderAdapter
|
||||||
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
import org.oxycblt.auxio.detail.list.DetailListAdapter
|
||||||
import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter
|
import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
@ -87,7 +90,17 @@ class PlaylistDetailFragment :
|
||||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
binding.detailRecycler.apply {
|
||||||
|
adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
||||||
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
|
if (it != 0) {
|
||||||
|
val item = detailModel.playlistList.value[it - 1]
|
||||||
|
item is Divider || item is Header
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
|
@ -126,6 +139,10 @@ class PlaylistDetailFragment :
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_rename -> {
|
||||||
|
musicModel.renamePlaylist(currentPlaylist)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
musicModel.deletePlaylist(currentPlaylist)
|
musicModel.deletePlaylist(currentPlaylist)
|
||||||
true
|
true
|
||||||
|
|
|
@ -69,15 +69,6 @@ class AlbumDetailListAdapter(private val listener: Listener<Song>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
|
||||||
if (super.isItemFullWidth(position)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// The album and disc headers should be full-width in all configurations.
|
|
||||||
val item = getItem(position)
|
|
||||||
return item is Album || item is Disc
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
|
|
|
@ -65,14 +65,6 @@ class ArtistDetailListAdapter(private val listener: Listener<Music>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
|
||||||
if (super.isItemFullWidth(position)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Artist headers should be full-width in all configurations.
|
|
||||||
return getItem(position) is Artist
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.Header
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
|
@ -47,13 +48,12 @@ import org.oxycblt.auxio.util.inflater
|
||||||
abstract class DetailListAdapter(
|
abstract class DetailListAdapter(
|
||||||
private val listener: Listener<*>,
|
private val listener: Listener<*>,
|
||||||
private val diffCallback: DiffUtil.ItemCallback<Item>
|
private val diffCallback: DiffUtil.ItemCallback<Item>
|
||||||
) :
|
) : SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(diffCallback) {
|
||||||
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(diffCallback),
|
|
||||||
AuxioRecyclerView.SpanSizeLookup {
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (getItem(position)) {
|
when (getItem(position)) {
|
||||||
// Implement support for headers and sort headers
|
// Implement support for headers and sort headers
|
||||||
|
is Divider -> DividerViewHolder.VIEW_TYPE
|
||||||
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
|
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
|
||||||
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
|
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
|
||||||
else -> super.getItemViewType(position)
|
else -> super.getItemViewType(position)
|
||||||
|
@ -61,6 +61,7 @@ abstract class DetailListAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
|
DividerViewHolder.VIEW_TYPE -> DividerViewHolder.from(parent)
|
||||||
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
|
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
|
||||||
SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.from(parent)
|
SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.from(parent)
|
||||||
else -> error("Invalid item type $viewType")
|
else -> error("Invalid item type $viewType")
|
||||||
|
@ -73,12 +74,6 @@ abstract class DetailListAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
|
||||||
// Headers should be full-width in all configurations.
|
|
||||||
val item = getItem(position)
|
|
||||||
return item is BasicHeader || item is SortHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An extended [SelectableListListener] for [DetailListAdapter] implementations. */
|
/** An extended [SelectableListListener] for [DetailListAdapter] implementations. */
|
||||||
interface Listener<in T : Music> : SelectableListListener<T> {
|
interface Listener<in T : Music> : SelectableListListener<T> {
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +89,8 @@ abstract class DetailListAdapter(
|
||||||
object : SimpleDiffCallback<Item>() {
|
object : SimpleDiffCallback<Item>() {
|
||||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
|
oldItem is Divider && newItem is Divider ->
|
||||||
|
DividerViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is BasicHeader && newItem is BasicHeader ->
|
oldItem is BasicHeader && newItem is BasicHeader ->
|
||||||
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is SortHeader && newItem is SortHeader ->
|
oldItem is SortHeader && newItem is SortHeader ->
|
||||||
|
|
|
@ -60,14 +60,6 @@ class GenreDetailListAdapter(private val listener: Listener<Music>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
|
||||||
if (super.isItemFullWidth(position)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Genre headers should be full-width in all configurations
|
|
||||||
return getItem(position) is Genre
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Item>() {
|
object : SimpleDiffCallback<Item>() {
|
||||||
|
|
|
@ -56,14 +56,6 @@ class PlaylistDetailListAdapter(private val listener: Listener<Song>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int): Boolean {
|
|
||||||
if (super.isItemFullWidth(position)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Playlist headers should be full-width in all configurations
|
|
||||||
return getItem(position) is Playlist
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Item>() {
|
object : SimpleDiffCallback<Item>() {
|
||||||
|
|
|
@ -36,12 +36,11 @@ import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
|
||||||
class SongKeyer @Inject constructor() : Keyer<Song> {
|
class SongKeyer @Inject constructor() : Keyer<Song> {
|
||||||
override fun key(data: Song, options: Options) =
|
override fun key(data: Song, options: Options) = "${data.album.uid}${data.album.hashCode()}"
|
||||||
"${data.album.uid}${data.album.songs.hashCode()}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParentKeyer @Inject constructor() : Keyer<MusicParent> {
|
class ParentKeyer @Inject constructor() : Keyer<MusicParent> {
|
||||||
override fun key(data: MusicParent, options: Options) = "${data.uid}${data.songs.hashCode()}"
|
override fun key(data: MusicParent, options: Options) = "${data.uid}${data.hashCode()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -40,3 +40,11 @@ interface Header : Item {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class BasicHeader(@StringRes override val titleRes: Int) : Header
|
data class BasicHeader(@StringRes override val titleRes: Int) : Header
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A divider decoration used to delimit groups of data.
|
||||||
|
*
|
||||||
|
* @param anchor The [Header] this divider should be next to in a list. Used as a way to preserve
|
||||||
|
* divider continuity during list updates.
|
||||||
|
*/
|
||||||
|
data class Divider(val anchor: Header?) : Item
|
||||||
|
|
|
@ -268,6 +268,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
playbackModel.addToQueue(playlist)
|
playbackModel.addToQueue(playlist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
|
R.id.action_rename -> {
|
||||||
|
musicModel.renamePlaylist(playlist)
|
||||||
|
}
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
musicModel.deletePlaylist(playlist)
|
musicModel.deletePlaylist(playlist)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,18 +23,14 @@ import android.util.AttributeSet
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView] with a few QoL extensions, such as:
|
* A [RecyclerView] with a few QoL extensions, such as:
|
||||||
* - Automatic edge-to-edge support
|
* - Automatic edge-to-edge support
|
||||||
* - Adapter-based [SpanSizeLookup] implementation
|
|
||||||
* - Automatic [setHasFixedSize] setup
|
* - Automatic [setHasFixedSize] setup
|
||||||
*
|
*
|
||||||
* FIXME: Broken span configuration
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
open class AuxioRecyclerView
|
open class AuxioRecyclerView
|
||||||
|
@ -49,7 +45,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
// Auxio's non-dialog RecyclerViews never change their size based on adapter contents,
|
// Auxio's non-dialog RecyclerViews never change their size based on adapter contents,
|
||||||
// so we can enable fixed-size optimizations.
|
// so we can enable fixed-size optimizations.
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
addItemDecoration(HeaderItemDecoration(context))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun setHasFixedSize(hasFixedSize: Boolean) {
|
final override fun setHasFixedSize(hasFixedSize: Boolean) {
|
||||||
|
@ -67,36 +62,4 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
updatePadding(bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
|
updatePadding(bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setAdapter(adapter: Adapter<*>?) {
|
|
||||||
super.setAdapter(adapter)
|
|
||||||
|
|
||||||
if (adapter is SpanSizeLookup) {
|
|
||||||
// This adapter has support for special span sizes, hook it up to the
|
|
||||||
// GridLayoutManager.
|
|
||||||
val glm = (layoutManager as GridLayoutManager)
|
|
||||||
val fullWidthSpanCount = glm.spanCount
|
|
||||||
glm.spanSizeLookup =
|
|
||||||
object : GridLayoutManager.SpanSizeLookup() {
|
|
||||||
// Using the adapter implementation, if the adapter specifies that
|
|
||||||
// an item is full width, it will take up all of the spans, using a
|
|
||||||
// single span otherwise.
|
|
||||||
override fun getSpanSize(position: Int) =
|
|
||||||
if (adapter.isItemFullWidth(position)) fullWidthSpanCount else 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A [RecyclerView.Adapter]-specific hook to control divider decoration visibility. */
|
|
||||||
|
|
||||||
/** An [RecyclerView.Adapter]-specific hook to [GridLayoutManager.SpanSizeLookup]. */
|
|
||||||
interface SpanSizeLookup {
|
|
||||||
/**
|
|
||||||
* Get if the item at a position takes up the whole width of the [RecyclerView] or not.
|
|
||||||
*
|
|
||||||
* @param position The position of the item.
|
|
||||||
* @return true if the item is full-width, false otherwise.
|
|
||||||
*/
|
|
||||||
fun isItemFullWidth(position: Int): Boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 Auxio Project
|
|
||||||
* HeaderItemDecoration.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.list.recycler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.list.Header
|
|
||||||
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
|
|
||||||
* separate content with headers.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
class HeaderItemDecoration
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(
|
|
||||||
context: Context,
|
|
||||||
attributeSet: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = R.attr.materialDividerStyle,
|
|
||||||
orientation: Int = LinearLayoutManager.VERTICAL
|
|
||||||
) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
|
|
||||||
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?): Boolean {
|
|
||||||
if (adapter is ConcatAdapter) {
|
|
||||||
val adapterAndPosition =
|
|
||||||
try {
|
|
||||||
adapter.getWrappedAdapterAndPosition(position + 1)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasHeaderAtPosition(adapterAndPosition.second, adapterAndPosition.first)
|
|
||||||
} else {
|
|
||||||
return hasHeaderAtPosition(position + 1, adapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasHeaderAtPosition(position: Int, adapter: RecyclerView.Adapter<*>?) =
|
|
||||||
try {
|
|
||||||
// Add a divider if the next item is a header. This organizes the divider to separate
|
|
||||||
// the ends of content rather than the beginning of content, alongside an added benefit
|
|
||||||
// of preventing top headers from having a divider applied.
|
|
||||||
(adapter as FlexibleListAdapter<*, *>).getItem(position) is Header
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
false
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,12 +20,14 @@ package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.divider.MaterialDivider
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||||
|
@ -246,7 +248,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Genre>() {
|
object : SimpleDiffCallback<Genre>() {
|
||||||
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
|
||||||
oldItem.name == newItem.name &&
|
oldItem.name == newItem.name &&
|
||||||
oldItem.artists.size == newItem.artists.size &&
|
oldItem.artists.size == newItem.artists.size &&
|
||||||
oldItem.songs.size == newItem.songs.size
|
oldItem.songs.size == newItem.songs.size
|
||||||
|
@ -304,7 +306,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<Playlist>() {
|
object : SimpleDiffCallback<Playlist>() {
|
||||||
override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist): Boolean =
|
override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist) =
|
||||||
oldItem.name == newItem.name && oldItem.songs.size == newItem.songs.size
|
oldItem.name == newItem.name && oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,10 +345,37 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
object : SimpleDiffCallback<BasicHeader>() {
|
object : SimpleDiffCallback<BasicHeader>() {
|
||||||
override fun areContentsTheSame(
|
override fun areContentsTheSame(oldItem: BasicHeader, newItem: BasicHeader) =
|
||||||
oldItem: BasicHeader,
|
oldItem.titleRes == newItem.titleRes
|
||||||
newItem: BasicHeader
|
}
|
||||||
): Boolean = oldItem.titleRes == newItem.titleRes
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecyclerView.ViewHolder] that displays a [Divider]. Use [from] to create an instance.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class DividerViewHolder private constructor(divider: MaterialDivider) :
|
||||||
|
RecyclerView.ViewHolder(divider) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Unique ID for this ViewHolder type. */
|
||||||
|
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_DIVIDER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
*
|
||||||
|
* @param parent The parent to inflate this instance from.
|
||||||
|
* @return A new instance.
|
||||||
|
*/
|
||||||
|
fun from(parent: View) = DividerViewHolder(MaterialDivider(parent.context))
|
||||||
|
|
||||||
|
/** A comparator that can be used with DiffUtil. */
|
||||||
|
val DIFF_CALLBACK =
|
||||||
|
object : SimpleDiffCallback<Divider>() {
|
||||||
|
override fun areContentsTheSame(oldItem: Divider, newItem: Divider) =
|
||||||
|
oldItem.anchor == newItem.anchor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,16 @@ sealed interface Music : Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates an Auxio-style [UID] of random composition. Used if there is no
|
||||||
|
* non-subjective, unlikely-to-change metadata of the music.
|
||||||
|
*
|
||||||
|
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||||
|
*/
|
||||||
|
fun auxio(mode: MusicMode): UID {
|
||||||
|
return UID(Format.AUXIO, mode, UUID.randomUUID())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
||||||
* unlikely-to-change metadata of the music.
|
* unlikely-to-change metadata of the music.
|
||||||
|
|
|
@ -118,6 +118,14 @@ interface MusicRepository {
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(name: String, songs: List<Song>)
|
fun createPlaylist(name: String, songs: List<Song>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a [Playlist].
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] to rename.
|
||||||
|
* @param name The name of the new [Playlist].
|
||||||
|
*/
|
||||||
|
fun renamePlaylist(playlist: Playlist, name: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a [Playlist].
|
* Delete a [Playlist].
|
||||||
*
|
*
|
||||||
|
@ -269,6 +277,15 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renamePlaylist(playlist: Playlist, name: String) {
|
||||||
|
val userLibrary = userLibrary ?: return
|
||||||
|
userLibrary.renamePlaylist(playlist, name)
|
||||||
|
for (listener in updateListeners) {
|
||||||
|
listener.onMusicChanges(
|
||||||
|
MusicRepository.Changes(deviceLibrary = false, userLibrary = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun deletePlaylist(playlist: Playlist) {
|
override fun deletePlaylist(playlist: Playlist) {
|
||||||
val userLibrary = userLibrary ?: return
|
val userLibrary = userLibrary ?: return
|
||||||
userLibrary.deletePlaylist(playlist)
|
userLibrary.deletePlaylist(playlist)
|
||||||
|
|
|
@ -52,15 +52,20 @@ constructor(
|
||||||
/** Flag for opening a dialog to create a playlist of the given [Song]s. */
|
/** Flag for opening a dialog to create a playlist of the given [Song]s. */
|
||||||
val newPlaylistSongs: Event<List<Song>> = _newPlaylistSongs
|
val newPlaylistSongs: Event<List<Song>> = _newPlaylistSongs
|
||||||
|
|
||||||
private val _songsToAdd = MutableEvent<List<Song>>()
|
private val _playlistToRename = MutableEvent<Playlist?>()
|
||||||
/** Flag for opening a dialog to add the given [Song]s to a playlist. */
|
/** Flag for opening a dialog to rename the given [Playlist]. */
|
||||||
val songsToAdd: Event<List<Song>> = _songsToAdd
|
val playlistToRename: Event<Playlist?>
|
||||||
|
get() = _playlistToRename
|
||||||
|
|
||||||
private val _playlistToDelete = MutableEvent<Playlist>()
|
private val _playlistToDelete = MutableEvent<Playlist>()
|
||||||
/** Flag for opening a dialog to confirm deletion of the given [Playlist]. */
|
/** Flag for opening a dialog to confirm deletion of the given [Playlist]. */
|
||||||
val playlistToDelete: Event<Playlist>
|
val playlistToDelete: Event<Playlist>
|
||||||
get() = _playlistToDelete
|
get() = _playlistToDelete
|
||||||
|
|
||||||
|
private val _songsToAdd = MutableEvent<List<Song>>()
|
||||||
|
/** Flag for opening a dialog to add the given [Song]s to a playlist. */
|
||||||
|
val songsToAdd: Event<List<Song>> = _songsToAdd
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
musicRepository.addIndexingListener(this)
|
musicRepository.addIndexingListener(this)
|
||||||
|
@ -111,6 +116,20 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the given playlist.
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] to rename,
|
||||||
|
* @param name The new name of the [Playlist]. If null, the user will be prompted for a name.
|
||||||
|
*/
|
||||||
|
fun renamePlaylist(playlist: Playlist, name: String? = null) {
|
||||||
|
if (name != null) {
|
||||||
|
musicRepository.renamePlaylist(playlist, name)
|
||||||
|
} else {
|
||||||
|
_playlistToRename.put(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a [Playlist].
|
* Delete a [Playlist].
|
||||||
*
|
*
|
||||||
|
|
|
@ -73,7 +73,7 @@ class AddToPlaylistDialog :
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
pickerModel.setSongsToAdd(args.songUids)
|
pickerModel.setSongsToAdd(args.songUids)
|
||||||
collectImmediately(pickerModel.currentSongsToAdd, ::updatePendingSongs)
|
collectImmediately(pickerModel.currentSongsToAdd, ::updatePendingSongs)
|
||||||
collectImmediately(pickerModel.playlistChoices, ::updatePlaylistChoices)
|
collectImmediately(pickerModel.playlistAddChoices, ::updatePlaylistChoices)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
|
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +55,7 @@ class DeletePlaylistDialog : ViewBindingDialogFragment<DialogDeletePlaylistBindi
|
||||||
// Now we can delete the playlist for-real this time.
|
// Now we can delete the playlist for-real this time.
|
||||||
musicModel.deletePlaylist(
|
musicModel.deletePlaylist(
|
||||||
unlikelyToBeNull(pickerModel.currentPlaylistToDelete.value), rude = true)
|
unlikelyToBeNull(pickerModel.currentPlaylistToDelete.value), rude = true)
|
||||||
|
requireContext().showToast(R.string.lng_playlist_deleted)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog allowing the name of a new/existing playlist to be edited.
|
* A dialog allowing the name of a new playlist to be chosen before committing it to the database.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -41,24 +41,34 @@ import org.oxycblt.auxio.music.Song
|
||||||
class PlaylistPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class PlaylistPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _currentPendingPlaylist = MutableStateFlow<PendingPlaylist?>(null)
|
private val _currentPendingPlaylist = MutableStateFlow<PendingPlaylist?>(null)
|
||||||
|
/** A new [Playlist] having it's name chosen by the user. Null if none yet. */
|
||||||
val currentPendingPlaylist: StateFlow<PendingPlaylist?>
|
val currentPendingPlaylist: StateFlow<PendingPlaylist?>
|
||||||
get() = _currentPendingPlaylist
|
get() = _currentPendingPlaylist
|
||||||
|
|
||||||
|
private val _currentPlaylistToRename = MutableStateFlow<Playlist?>(null)
|
||||||
|
/** An existing [Playlist] that is being renamed. Null if none yet. */
|
||||||
|
val currentPlaylistToRename: StateFlow<Playlist?>
|
||||||
|
get() = _currentPlaylistToRename
|
||||||
|
|
||||||
|
private val _currentPlaylistToDelete = MutableStateFlow<Playlist?>(null)
|
||||||
|
/** The current [Playlist] that needs it's deletion confirmed. Null if none yet. */
|
||||||
|
val currentPlaylistToDelete: StateFlow<Playlist?>
|
||||||
|
get() = _currentPlaylistToDelete
|
||||||
|
|
||||||
private val _chosenName = MutableStateFlow<ChosenName>(ChosenName.Empty)
|
private val _chosenName = MutableStateFlow<ChosenName>(ChosenName.Empty)
|
||||||
|
/** The users chosen name for [currentPendingPlaylist] or [currentPlaylistToRename]. */
|
||||||
val chosenName: StateFlow<ChosenName>
|
val chosenName: StateFlow<ChosenName>
|
||||||
get() = _chosenName
|
get() = _chosenName
|
||||||
|
|
||||||
private val _currentSongsToAdd = MutableStateFlow<List<Song>?>(null)
|
private val _currentSongsToAdd = MutableStateFlow<List<Song>?>(null)
|
||||||
|
/** A batch of [Song]s to add to a playlist chosen by the user. Null if none yet. */
|
||||||
val currentSongsToAdd: StateFlow<List<Song>?>
|
val currentSongsToAdd: StateFlow<List<Song>?>
|
||||||
get() = _currentSongsToAdd
|
get() = _currentSongsToAdd
|
||||||
|
|
||||||
private val _playlistChoices = MutableStateFlow<List<PlaylistChoice>>(listOf())
|
private val _playlistAddChoices = MutableStateFlow<List<PlaylistChoice>>(listOf())
|
||||||
val playlistChoices: StateFlow<List<PlaylistChoice>>
|
/** The [Playlist]s that [currentSongsToAdd] could be added to. */
|
||||||
get() = _playlistChoices
|
val playlistAddChoices: StateFlow<List<PlaylistChoice>>
|
||||||
|
get() = _playlistAddChoices
|
||||||
private val _currentPlaylistToDelete = MutableStateFlow<Playlist?>(null)
|
|
||||||
val currentPlaylistToDelete: StateFlow<Playlist?>
|
|
||||||
get() = _currentPlaylistToDelete
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
|
@ -124,6 +134,24 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new [currentPlaylistToRename] from a [Playlist] [Music.UID].
|
||||||
|
*
|
||||||
|
* @param playlistUid The [Music.UID]s of the [Playlist] to rename.
|
||||||
|
*/
|
||||||
|
fun setPlaylistToRename(playlistUid: Music.UID) {
|
||||||
|
_currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new [currentPendingPlaylist] from a new [Playlist] [Music.UID].
|
||||||
|
*
|
||||||
|
* @param playlistUid The [Music.UID] of the [Playlist] to delete.
|
||||||
|
*/
|
||||||
|
fun setPlaylistToDelete(playlistUid: Music.UID) {
|
||||||
|
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current [chosenName] based on new user input.
|
* Update the current [chosenName] based on new user input.
|
||||||
*
|
*
|
||||||
|
@ -160,21 +188,12 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
|
||||||
|
|
||||||
private fun refreshPlaylistChoices(songs: List<Song>) {
|
private fun refreshPlaylistChoices(songs: List<Song>) {
|
||||||
val userLibrary = musicRepository.userLibrary ?: return
|
val userLibrary = musicRepository.userLibrary ?: return
|
||||||
_playlistChoices.value =
|
_playlistAddChoices.value =
|
||||||
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map {
|
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map {
|
||||||
val songSet = it.songs.toSet()
|
val songSet = it.songs.toSet()
|
||||||
PlaylistChoice(it, songs.all(songSet::contains))
|
PlaylistChoice(it, songs.all(songSet::contains))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a new [currentPendingPlaylist] from a new [Playlist] [Music.UID].
|
|
||||||
*
|
|
||||||
* @param playlistUid The [Music.UID] of the [Playlist] to delete.
|
|
||||||
*/
|
|
||||||
fun setPlaylistToDelete(playlistUid: Music.UID) {
|
|
||||||
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* RenamePlaylistDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music.picker
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog allowing the name of a new playlist to be chosen before committing it to the database.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class RenamePlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() {
|
||||||
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
private val pickerModel: PlaylistPickerViewModel by viewModels()
|
||||||
|
// Information about what playlist to name for is initially within the navigation arguments
|
||||||
|
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||||
|
private val args: RenamePlaylistDialogArgs by navArgs()
|
||||||
|
private var initializedField = false
|
||||||
|
|
||||||
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder
|
||||||
|
.setTitle(R.string.lbl_rename)
|
||||||
|
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||||
|
val playlist = unlikelyToBeNull(pickerModel.currentPlaylistToRename.value)
|
||||||
|
val chosenName = pickerModel.chosenName.value as ChosenName.Valid
|
||||||
|
musicModel.renamePlaylist(playlist, chosenName.value)
|
||||||
|
requireContext().showToast(R.string.lng_playlist_renamed)
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
DialogPlaylistNameBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
|
binding.playlistName.addTextChangedListener { pickerModel.updateChosenName(it?.toString()) }
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
pickerModel.setPlaylistToRename(args.playlistUid)
|
||||||
|
collectImmediately(pickerModel.currentPlaylistToRename, ::updatePlaylistToRename)
|
||||||
|
collectImmediately(pickerModel.chosenName, ::updateChosenName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylistToRename(playlist: Playlist?) {
|
||||||
|
if (playlist == null) {
|
||||||
|
// Nothing to rename anymore.
|
||||||
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializedField) {
|
||||||
|
requireBinding().playlistName.setText(playlist.name.resolve(requireContext()))
|
||||||
|
initializedField = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChosenName(chosenName: ChosenName) {
|
||||||
|
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
||||||
|
chosenName is ChosenName.Valid
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.user
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.util.update
|
|
||||||
|
|
||||||
class PlaylistImpl
|
class PlaylistImpl
|
||||||
private constructor(
|
private constructor(
|
||||||
|
@ -33,6 +32,15 @@ private constructor(
|
||||||
override val albums =
|
override val albums =
|
||||||
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the data in this instance to a new [PlaylistImpl] with the given [name].
|
||||||
|
*
|
||||||
|
* @param name The new name to use.
|
||||||
|
* @param musicSettings [MusicSettings] required for name configuration.
|
||||||
|
*/
|
||||||
|
fun edit(name: String, musicSettings: MusicSettings) =
|
||||||
|
PlaylistImpl(uid, Name.Known.from(name, null, musicSettings), songs)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s.
|
* Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s.
|
||||||
*
|
*
|
||||||
|
@ -48,9 +56,14 @@ private constructor(
|
||||||
inline fun edit(edits: MutableList<Song>.() -> Unit) = edit(songs.toMutableList().apply(edits))
|
inline fun edit(edits: MutableList<Song>.() -> Unit) = edit(songs.toMutableList().apply(edits))
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is PlaylistImpl && uid == other.uid && songs == other.songs
|
other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
|
||||||
|
|
||||||
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
|
override fun hashCode(): Int {
|
||||||
|
var hashCode = uid.hashCode()
|
||||||
|
hashCode = 31 * hashCode + name.hashCode()
|
||||||
|
hashCode = 31 * hashCode + songs.hashCode()
|
||||||
|
return hashCode
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +75,7 @@ private constructor(
|
||||||
*/
|
*/
|
||||||
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
||||||
PlaylistImpl(
|
PlaylistImpl(
|
||||||
Music.UID.auxio(MusicMode.PLAYLISTS) { update(name) },
|
Music.UID.auxio(MusicMode.PLAYLISTS),
|
||||||
Name.Known.from(name, null, musicSettings),
|
Name.Known.from(name, null, musicSettings),
|
||||||
songs)
|
songs)
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,14 @@ interface MutableUserLibrary : UserLibrary {
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(name: String, songs: List<Song>)
|
fun createPlaylist(name: String, songs: List<Song>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a [Playlist].
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] to rename.
|
||||||
|
* @param name The name of the new [Playlist].
|
||||||
|
*/
|
||||||
|
fun renamePlaylist(playlist: Playlist, name: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a [Playlist].
|
* Delete a [Playlist].
|
||||||
*
|
*
|
||||||
|
@ -122,6 +130,13 @@ private class UserLibraryImpl(
|
||||||
playlistMap[playlistImpl.uid] = playlistImpl
|
playlistMap[playlistImpl.uid] = playlistImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun renamePlaylist(playlist: Playlist, name: String) {
|
||||||
|
val playlistImpl =
|
||||||
|
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
|
||||||
|
playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings)
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun deletePlaylist(playlist: Playlist) {
|
override fun deletePlaylist(playlist: Playlist) {
|
||||||
playlistMap.remove(playlist.uid)
|
playlistMap.remove(playlist.uid)
|
||||||
|
|
|
@ -34,8 +34,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(DIFF_CALLBACK),
|
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
|
||||||
AuxioRecyclerView.SpanSizeLookup {
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (getItem(position)) {
|
when (getItem(position)) {
|
||||||
|
@ -44,6 +43,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
is Artist -> ArtistViewHolder.VIEW_TYPE
|
is Artist -> ArtistViewHolder.VIEW_TYPE
|
||||||
is Genre -> GenreViewHolder.VIEW_TYPE
|
is Genre -> GenreViewHolder.VIEW_TYPE
|
||||||
is Playlist -> PlaylistViewHolder.VIEW_TYPE
|
is Playlist -> PlaylistViewHolder.VIEW_TYPE
|
||||||
|
is Divider -> DividerViewHolder.VIEW_TYPE
|
||||||
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
|
is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE
|
||||||
else -> super.getItemViewType(position)
|
else -> super.getItemViewType(position)
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent)
|
ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent)
|
||||||
GenreViewHolder.VIEW_TYPE -> GenreViewHolder.from(parent)
|
GenreViewHolder.VIEW_TYPE -> GenreViewHolder.from(parent)
|
||||||
PlaylistViewHolder.VIEW_TYPE -> PlaylistViewHolder.from(parent)
|
PlaylistViewHolder.VIEW_TYPE -> PlaylistViewHolder.from(parent)
|
||||||
|
DividerViewHolder.VIEW_TYPE -> DividerViewHolder.from(parent)
|
||||||
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
|
BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent)
|
||||||
else -> error("Invalid item type $viewType")
|
else -> error("Invalid item type $viewType")
|
||||||
}
|
}
|
||||||
|
@ -71,8 +72,6 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isItemFullWidth(position: Int) = getItem(position) is BasicHeader
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/** A comparator that can be used with DiffUtil. */
|
/** A comparator that can be used with DiffUtil. */
|
||||||
val DIFF_CALLBACK =
|
val DIFF_CALLBACK =
|
||||||
|
@ -87,6 +86,10 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||||
ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Genre && newItem is Genre ->
|
oldItem is Genre && newItem is Genre ->
|
||||||
GenreViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
GenreViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
|
oldItem is Playlist && newItem is Playlist ->
|
||||||
|
PlaylistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
|
oldItem is Divider && newItem is Divider ->
|
||||||
|
DividerViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is BasicHeader && newItem is BasicHeader ->
|
oldItem is BasicHeader && newItem is BasicHeader ->
|
||||||
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||||
else -> false
|
else -> false
|
||||||
|
|
|
@ -29,10 +29,13 @@ import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
@ -104,7 +107,13 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchRecycler.adapter = searchAdapter
|
binding.searchRecycler.apply {
|
||||||
|
adapter = searchAdapter
|
||||||
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
|
val item = searchModel.searchResults.value[it]
|
||||||
|
item is Divider || item is Header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
@ -138,23 +139,44 @@ constructor(
|
||||||
|
|
||||||
return buildList {
|
return buildList {
|
||||||
results.artists?.let {
|
results.artists?.let {
|
||||||
add(BasicHeader(R.string.lbl_artists))
|
val header = BasicHeader(R.string.lbl_artists)
|
||||||
|
add(header)
|
||||||
addAll(SORT.artists(it))
|
addAll(SORT.artists(it))
|
||||||
}
|
}
|
||||||
results.albums?.let {
|
results.albums?.let {
|
||||||
add(BasicHeader(R.string.lbl_albums))
|
val header = BasicHeader(R.string.lbl_albums)
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
add(Divider(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(header)
|
||||||
addAll(SORT.albums(it))
|
addAll(SORT.albums(it))
|
||||||
}
|
}
|
||||||
results.playlists?.let {
|
results.playlists?.let {
|
||||||
add(BasicHeader(R.string.lbl_playlists))
|
val header = BasicHeader(R.string.lbl_playlists)
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
add(Divider(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(header)
|
||||||
addAll(SORT.playlists(it))
|
addAll(SORT.playlists(it))
|
||||||
}
|
}
|
||||||
results.genres?.let {
|
results.genres?.let {
|
||||||
add(BasicHeader(R.string.lbl_genres))
|
val header = BasicHeader(R.string.lbl_genres)
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
add(Divider(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(header)
|
||||||
addAll(SORT.genres(it))
|
addAll(SORT.genres(it))
|
||||||
}
|
}
|
||||||
results.songs?.let {
|
results.songs?.let {
|
||||||
add(BasicHeader(R.string.lbl_songs))
|
val header = BasicHeader(R.string.lbl_songs)
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
add(Divider(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(header)
|
||||||
addAll(SORT.songs(it))
|
addAll(SORT.songs(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import androidx.core.graphics.Insets
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
@ -111,6 +112,20 @@ val ViewBinding.context: Context
|
||||||
*/
|
*/
|
||||||
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
|
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut to easily set up a [GridLayoutManager.SpanSizeLookup].
|
||||||
|
*
|
||||||
|
* @param isItemFullWidth Mapping expression that returns true if the item should take up all spans
|
||||||
|
* or just one.
|
||||||
|
*/
|
||||||
|
fun GridLayoutManager.setFullWidthLookup(isItemFullWidth: (Int) -> Boolean) {
|
||||||
|
spanSizeLookup =
|
||||||
|
object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position: Int) =
|
||||||
|
if (isItemFullWidth(position)) spanCount else 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fix the double ripple that appears in MaterialButton instances due to an issue with AppCompat 1.5
|
* Fix the double ripple that appears in MaterialButton instances due to an issue with AppCompat 1.5
|
||||||
* or higher.
|
* or higher.
|
||||||
|
|
4
app/src/main/res/layout/item_divider.xml
Normal file
4
app/src/main/res/layout/item_divider.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.divider.MaterialDivider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
|
@ -13,9 +13,12 @@
|
||||||
android:id="@+id/action_queue_add"
|
android:id="@+id/action_queue_add"
|
||||||
android:title="@string/lbl_queue_add" />
|
android:title="@string/lbl_queue_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_share"
|
android:id="@+id/action_rename"
|
||||||
android:title="@string/lbl_share" />
|
android:title="@string/lbl_rename" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_delete"
|
android:id="@+id/action_delete"
|
||||||
android:title="@string/lbl_delete" />
|
android:title="@string/lbl_delete" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:title="@string/lbl_share" />
|
||||||
</menu>
|
</menu>
|
|
@ -7,9 +7,12 @@
|
||||||
android:id="@+id/action_queue_add"
|
android:id="@+id/action_queue_add"
|
||||||
android:title="@string/lbl_queue_add" />
|
android:title="@string/lbl_queue_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_share"
|
android:id="@+id/action_rename"
|
||||||
android:title="@string/lbl_share" />
|
android:title="@string/lbl_rename" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_delete"
|
android:id="@+id/action_delete"
|
||||||
android:title="@string/lbl_delete" />
|
android:title="@string/lbl_delete" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:title="@string/lbl_share" />
|
||||||
</menu>
|
</menu>
|
|
@ -21,11 +21,14 @@
|
||||||
android:id="@+id/action_new_playlist"
|
android:id="@+id/action_new_playlist"
|
||||||
app:destination="@id/new_playlist_dialog" />
|
app:destination="@id/new_playlist_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_add_to_playlist"
|
android:id="@+id/action_rename_playlist"
|
||||||
app:destination="@id/add_to_playlist_dialog" />
|
app:destination="@id/rename_playlist_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_delete_playlist"
|
android:id="@+id/action_delete_playlist"
|
||||||
app:destination="@id/delete_playlist_dialog" />
|
app:destination="@id/delete_playlist_dialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_add_to_playlist"
|
||||||
|
app:destination="@id/add_to_playlist_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_navigation_artist"
|
android:id="@+id/action_pick_navigation_artist"
|
||||||
app:destination="@id/navigate_to_artist_dialog" />
|
app:destination="@id/navigate_to_artist_dialog" />
|
||||||
|
@ -58,16 +61,13 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/add_to_playlist_dialog"
|
android:id="@+id/rename_playlist_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.picker.AddToPlaylistDialog"
|
android:name="org.oxycblt.auxio.music.picker.RenamePlaylistDialog"
|
||||||
android:label="new_playlist_dialog"
|
android:label="rename_playlist_dialog"
|
||||||
tools:layout="@layout/dialog_playlist_name">
|
tools:layout="@layout/dialog_playlist_name">
|
||||||
<argument
|
<argument
|
||||||
android:name="songUids"
|
android:name="playlistUid"
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
<action
|
|
||||||
android:id="@+id/action_new_playlist"
|
|
||||||
app:destination="@id/new_playlist_dialog" />
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
@ -80,6 +80,19 @@
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/add_to_playlist_dialog"
|
||||||
|
android:name="org.oxycblt.auxio.music.picker.AddToPlaylistDialog"
|
||||||
|
android:label="new_playlist_dialog"
|
||||||
|
tools:layout="@layout/dialog_playlist_name">
|
||||||
|
<argument
|
||||||
|
android:name="songUids"
|
||||||
|
app:argType="org.oxycblt.auxio.music.Music$UID[]" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_new_playlist"
|
||||||
|
app:destination="@id/new_playlist_dialog" />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/navigate_to_artist_dialog"
|
android:id="@+id/navigate_to_artist_dialog"
|
||||||
android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
|
android:name="org.oxycblt.auxio.navigation.picker.NavigateToArtistDialog"
|
||||||
|
|
|
@ -79,6 +79,8 @@
|
||||||
<string name="lbl_playlist">Playlist</string>
|
<string name="lbl_playlist">Playlist</string>
|
||||||
<string name="lbl_playlists">Playlists</string>
|
<string name="lbl_playlists">Playlists</string>
|
||||||
<string name="lbl_new_playlist">New playlist</string>
|
<string name="lbl_new_playlist">New playlist</string>
|
||||||
|
<string name="lbl_rename">Rename</string>
|
||||||
|
<string name="lbl_rename_playlist">Rename playlist</string>
|
||||||
<string name="lbl_delete">Delete</string>
|
<string name="lbl_delete">Delete</string>
|
||||||
<string name="lbl_confirm_delete_playlist">Delete playlist?</string>
|
<string name="lbl_confirm_delete_playlist">Delete playlist?</string>
|
||||||
|
|
||||||
|
@ -166,6 +168,8 @@
|
||||||
<string name="lng_observing">Monitoring your music library for changes…</string>
|
<string name="lng_observing">Monitoring your music library for changes…</string>
|
||||||
<string name="lng_queue_added">Added to queue</string>
|
<string name="lng_queue_added">Added to queue</string>
|
||||||
<string name="lng_playlist_created">Playlist created</string>
|
<string name="lng_playlist_created">Playlist created</string>
|
||||||
|
<string name="lng_playlist_renamed">Playlist renamed</string>
|
||||||
|
<string name="lng_playlist_deleted">Playlist deleted</string>
|
||||||
<string name="lng_playlist_added">Added to playlist</string>
|
<string name="lng_playlist_added">Added to playlist</string>
|
||||||
<string name="lng_author">Developed by Alexander Capehart</string>
|
<string name="lng_author">Developed by Alexander Capehart</string>
|
||||||
<!-- As in music library -->
|
<!-- As in music library -->
|
||||||
|
|
|
@ -70,6 +70,10 @@ open class FakeMusicRepository : MusicRepository {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renamePlaylist(playlist: Playlist, name: String) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
override fun requestIndex(withCache: Boolean) {
|
override fun requestIndex(withCache: Boolean) {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue