playlist: add basic ui support
Add extremely basic UI support for playlists.
This commit is contained in:
parent
9a282e2be9
commit
686290a6c1
33 changed files with 505 additions and 133 deletions
|
@ -33,18 +33,20 @@ object IntegerTable {
|
||||||
const val VIEW_TYPE_ARTIST = 0xA002
|
const val VIEW_TYPE_ARTIST = 0xA002
|
||||||
/** GenreViewHolder */
|
/** GenreViewHolder */
|
||||||
const val VIEW_TYPE_GENRE = 0xA003
|
const val VIEW_TYPE_GENRE = 0xA003
|
||||||
|
/** PlaylistViewHolder */
|
||||||
|
const val VIEW_TYPE_PLAYLIST = 0xA004
|
||||||
/** BasicHeaderViewHolder */
|
/** BasicHeaderViewHolder */
|
||||||
const val VIEW_TYPE_BASIC_HEADER = 0xA004
|
const val VIEW_TYPE_BASIC_HEADER = 0xA005
|
||||||
/** SortHeaderViewHolder */
|
/** SortHeaderViewHolder */
|
||||||
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
const val VIEW_TYPE_SORT_HEADER = 0xA006
|
||||||
/** AlbumSongViewHolder */
|
/** AlbumSongViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
||||||
/** ArtistAlbumViewHolder */
|
/** ArtistAlbumViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
const val VIEW_TYPE_ARTIST_ALBUM = 0xA008
|
||||||
/** ArtistSongViewHolder */
|
/** ArtistSongViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
const val VIEW_TYPE_ARTIST_SONG = 0xA009
|
||||||
/** DiscHeaderViewHolder */
|
/** DiscHeaderViewHolder */
|
||||||
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
const val VIEW_TYPE_DISC_HEADER = 0xA00A
|
||||||
/** "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 */
|
||||||
|
@ -65,16 +67,16 @@ object IntegerTable {
|
||||||
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
||||||
/** PlaybackMode.ALL_SONGS */
|
/** PlaybackMode.ALL_SONGS */
|
||||||
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
||||||
/** DisplayMode.NONE (No Longer used but still reserved) */
|
|
||||||
// const val DISPLAY_MODE_NONE = 0xA107
|
|
||||||
/** MusicMode._GENRES */
|
|
||||||
const val MUSIC_MODE_GENRES = 0xA108
|
|
||||||
/** MusicMode._ARTISTS */
|
|
||||||
const val MUSIC_MODE_ARTISTS = 0xA109
|
|
||||||
/** MusicMode._ALBUMS */
|
|
||||||
const val MUSIC_MODE_ALBUMS = 0xA10A
|
|
||||||
/** MusicMode.SONGS */
|
/** MusicMode.SONGS */
|
||||||
const val MUSIC_MODE_SONGS = 0xA10B
|
const val MUSIC_MODE_SONGS = 0xA10B
|
||||||
|
/** MusicMode.ALBUMS */
|
||||||
|
const val MUSIC_MODE_ALBUMS = 0xA10A
|
||||||
|
/** MusicMode.ARTISTS */
|
||||||
|
const val MUSIC_MODE_ARTISTS = 0xA109
|
||||||
|
/** MusicMode.GENRES */
|
||||||
|
const val MUSIC_MODE_GENRES = 0xA108
|
||||||
|
/** MusicMode.PLAYLISTS */
|
||||||
|
const val MUSIC_MODE_PLAYLISTS = 0xA107
|
||||||
/** Sort.ByName */
|
/** Sort.ByName */
|
||||||
const val SORT_BY_NAME = 0xA10C
|
const val SORT_BY_NAME = 0xA10C
|
||||||
/** Sort.ByArtist */
|
/** Sort.ByArtist */
|
||||||
|
|
|
@ -149,7 +149,7 @@ class GenreDetailFragment :
|
||||||
|
|
||||||
override fun onOpenMenu(item: Music, anchor: View) {
|
override fun onOpenMenu(item: Music, anchor: View) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
||||||
import org.oxycblt.auxio.home.list.AlbumListFragment
|
import org.oxycblt.auxio.home.list.*
|
||||||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
|
||||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
|
||||||
import org.oxycblt.auxio.home.list.SongListFragment
|
|
||||||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.list.selection.SelectionFragment
|
import org.oxycblt.auxio.list.selection.SelectionFragment
|
||||||
|
@ -278,16 +275,8 @@ class HomeFragment :
|
||||||
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
|
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
|
||||||
// Disallow sorting by album for albums
|
// Disallow sorting by album for albums
|
||||||
MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album }
|
MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album }
|
||||||
// Only allow sorting by name, count, and duration for artists
|
// Only allow sorting by name, count, and duration for parents
|
||||||
MusicMode.ARTISTS -> { id ->
|
else -> { id ->
|
||||||
id == R.id.option_sort_asc ||
|
|
||||||
id == R.id.option_sort_dec ||
|
|
||||||
id == R.id.option_sort_name ||
|
|
||||||
id == R.id.option_sort_count ||
|
|
||||||
id == R.id.option_sort_duration
|
|
||||||
}
|
|
||||||
// Only allow sorting by name, count, and duration for genres
|
|
||||||
MusicMode.GENRES -> { id ->
|
|
||||||
id == R.id.option_sort_asc ||
|
id == R.id.option_sort_asc ||
|
||||||
id == R.id.option_sort_dec ||
|
id == R.id.option_sort_dec ||
|
||||||
id == R.id.option_sort_name ||
|
id == R.id.option_sort_name ||
|
||||||
|
@ -325,6 +314,7 @@ class HomeFragment :
|
||||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
||||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||||
|
MusicMode.PLAYLISTS -> R.id.home_playlist_recycler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,6 +487,7 @@ class HomeFragment :
|
||||||
MusicMode.ALBUMS -> AlbumListFragment()
|
MusicMode.ALBUMS -> AlbumListFragment()
|
||||||
MusicMode.ARTISTS -> ArtistListFragment()
|
MusicMode.ARTISTS -> ArtistListFragment()
|
||||||
MusicMode.GENRES -> GenreListFragment()
|
MusicMode.GENRES -> GenreListFragment()
|
||||||
|
MusicMode.PLAYLISTS -> PlaylistListFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
@ -64,10 +65,29 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
|
||||||
override val shouldHideCollaborators: Boolean
|
override val shouldHideCollaborators: Boolean
|
||||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_hide_collaborators), false)
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_hide_collaborators), false)
|
||||||
|
|
||||||
|
override fun migrate() {
|
||||||
|
if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) {
|
||||||
|
val oldTabs =
|
||||||
|
Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
|
||||||
|
?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT))
|
||||||
|
|
||||||
|
// Add the new playlist tab to old tab configurations
|
||||||
|
val correctedTabs = oldTabs + Tab.Visible(MusicMode.PLAYLISTS)
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(correctedTabs))
|
||||||
|
remove(OLD_KEY_LIB_TABS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSettingChanged(key: String, listener: HomeSettings.Listener) {
|
override fun onSettingChanged(key: String, listener: HomeSettings.Listener) {
|
||||||
when (key) {
|
when (key) {
|
||||||
getString(R.string.set_key_home_tabs) -> listener.onTabsChanged()
|
getString(R.string.set_key_home_tabs) -> listener.onTabsChanged()
|
||||||
getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged()
|
getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val OLD_KEY_LIB_TABS = "auxio_lib_tabs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,15 @@ constructor(
|
||||||
val genresInstructions: Event<UpdateInstructions>
|
val genresInstructions: Event<UpdateInstructions>
|
||||||
get() = _genresInstructions
|
get() = _genresInstructions
|
||||||
|
|
||||||
|
private val _playlistsList = MutableStateFlow(listOf<Playlist>())
|
||||||
|
/** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
|
val playlistsList: StateFlow<List<Playlist>>
|
||||||
|
get() = _playlistsList
|
||||||
|
private val _playlistsInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [genresList] in the UI. */
|
||||||
|
val playlistsInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _playlistsInstructions
|
||||||
|
|
||||||
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
||||||
val playbackMode: MusicMode
|
val playbackMode: MusicMode
|
||||||
get() = playbackSettings.inListPlaybackMode
|
get() = playbackSettings.inListPlaybackMode
|
||||||
|
@ -127,26 +136,34 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
if (!changes.library) return
|
val library = musicRepository.library
|
||||||
val library = musicRepository.library ?: return
|
if (changes.library && library != null) {
|
||||||
logD("Library changed, refreshing library")
|
logD("Refreshing library")
|
||||||
// Get the each list of items in the library to use as our list data.
|
// Get the each list of items in the library to use as our list data.
|
||||||
// Applying the preferred sorting to them.
|
// Applying the preferred sorting to them.
|
||||||
_songsInstructions.put(UpdateInstructions.Diff)
|
_songsInstructions.put(UpdateInstructions.Diff)
|
||||||
_songsList.value = musicSettings.songSort.songs(library.songs)
|
_songsList.value = musicSettings.songSort.songs(library.songs)
|
||||||
_albumsInstructions.put(UpdateInstructions.Diff)
|
_albumsInstructions.put(UpdateInstructions.Diff)
|
||||||
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
||||||
_artistsInstructions.put(UpdateInstructions.Diff)
|
_artistsInstructions.put(UpdateInstructions.Diff)
|
||||||
_artistsList.value =
|
_artistsList.value =
|
||||||
musicSettings.artistSort.artists(
|
musicSettings.artistSort.artists(
|
||||||
if (homeSettings.shouldHideCollaborators) {
|
if (homeSettings.shouldHideCollaborators) {
|
||||||
// Hide Collaborators is enabled, filter out collaborators.
|
// Hide Collaborators is enabled, filter out collaborators.
|
||||||
library.artists.filter { !it.isCollaborator }
|
library.artists.filter { !it.isCollaborator }
|
||||||
} else {
|
} else {
|
||||||
library.artists
|
library.artists
|
||||||
})
|
})
|
||||||
_genresInstructions.put(UpdateInstructions.Diff)
|
_genresInstructions.put(UpdateInstructions.Diff)
|
||||||
_genresList.value = musicSettings.genreSort.genres(library.genres)
|
_genresList.value = musicSettings.genreSort.genres(library.genres)
|
||||||
|
}
|
||||||
|
|
||||||
|
val playlists = musicRepository.playlists
|
||||||
|
if (changes.playlists && playlists != null) {
|
||||||
|
logD("Refreshing playlists")
|
||||||
|
_playlistsInstructions.put(UpdateInstructions.Diff)
|
||||||
|
_playlistsList.value = musicSettings.playlistSort.playlists(playlists)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabsChanged() {
|
override fun onTabsChanged() {
|
||||||
|
@ -173,6 +190,7 @@ constructor(
|
||||||
MusicMode.ALBUMS -> musicSettings.albumSort
|
MusicMode.ALBUMS -> musicSettings.albumSort
|
||||||
MusicMode.ARTISTS -> musicSettings.artistSort
|
MusicMode.ARTISTS -> musicSettings.artistSort
|
||||||
MusicMode.GENRES -> musicSettings.genreSort
|
MusicMode.GENRES -> musicSettings.genreSort
|
||||||
|
MusicMode.PLAYLISTS -> musicSettings.playlistSort
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,6 +222,11 @@ constructor(
|
||||||
_genresInstructions.put(UpdateInstructions.Replace(0))
|
_genresInstructions.put(UpdateInstructions.Replace(0))
|
||||||
_genresList.value = sort.genres(_genresList.value)
|
_genresList.value = sort.genres(_genresList.value)
|
||||||
}
|
}
|
||||||
|
MusicMode.PLAYLISTS -> {
|
||||||
|
musicSettings.playlistSort = sort
|
||||||
|
_playlistsInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_playlistsList.value = sort.playlists(_playlistsList.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@ class ArtistListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Artist, anchor: View) {
|
override fun onOpenMenu(item: Artist, anchor: View) {
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateArtists(artists: List<Artist>) {
|
private fun updateArtists(artists: List<Artist>) {
|
||||||
|
|
|
@ -114,7 +114,7 @@ class GenreListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Genre, anchor: View) {
|
override fun onOpenMenu(item: Genre, anchor: View) {
|
||||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGenres(genres: List<Genre>) {
|
private fun updateGenres(genres: List<Genre>) {
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistListFragment.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
|
import org.oxycblt.auxio.list.*
|
||||||
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
|
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
|
||||||
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
class PlaylistListFragment :
|
||||||
|
ListFragment<Playlist, FragmentHomeListBinding>(),
|
||||||
|
FastScrollRecyclerView.PopupProvider,
|
||||||
|
FastScrollRecyclerView.Listener {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
override val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
override val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
|
private val playlistAdapter = PlaylistAdapter(this)
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.homeRecycler.apply {
|
||||||
|
id = R.id.home_playlist_recycler
|
||||||
|
adapter = playlistAdapter
|
||||||
|
popupProvider = this@PlaylistListFragment
|
||||||
|
listener = this@PlaylistListFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
collectImmediately(homeModel.playlistsList, ::updatePlaylists)
|
||||||
|
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||||
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||||
|
super.onDestroyBinding(binding)
|
||||||
|
binding.homeRecycler.apply {
|
||||||
|
adapter = null
|
||||||
|
popupProvider = null
|
||||||
|
listener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPopup(pos: Int): String? {
|
||||||
|
val playlist = homeModel.playlistsList.value[pos]
|
||||||
|
// Change how we display the popup depending on the current sort mode.
|
||||||
|
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
||||||
|
// By Name -> Use Name
|
||||||
|
is Sort.Mode.ByName -> playlist.sortName?.thumbString
|
||||||
|
|
||||||
|
// Duration -> Use formatted duration
|
||||||
|
is Sort.Mode.ByDuration -> playlist.durationMs.formatDurationMs(false)
|
||||||
|
|
||||||
|
// Count -> Use song count
|
||||||
|
is Sort.Mode.ByCount -> playlist.songs.size.toString()
|
||||||
|
|
||||||
|
// Unsupported sort, error gracefully
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFastScrollingChanged(isFastScrolling: Boolean) {
|
||||||
|
homeModel.setFastScrolling(isFastScrolling)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRealClick(item: Playlist) {
|
||||||
|
navModel.exploreNavigateTo(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenMenu(item: Playlist, anchor: View) {
|
||||||
|
openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylists(playlists: List<Playlist>) {
|
||||||
|
playlistAdapter.update(
|
||||||
|
playlists, homeModel.playlistsInstructions.consume().also { logD(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
playlistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
|
// If a playlist is playing, highlight it within this adapter.
|
||||||
|
playlistAdapter.setPlaying(parent as? Playlist, isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SelectionIndicatorAdapter] that shows a list of [Playlist]s using [PlaylistViewHolder].
|
||||||
|
*
|
||||||
|
* @param listener An [SelectableListListener] to bind interactions to.
|
||||||
|
*/
|
||||||
|
private class PlaylistAdapter(private val listener: SelectableListListener<Playlist>) :
|
||||||
|
SelectionIndicatorAdapter<Playlist, PlaylistViewHolder>(PlaylistViewHolder.DIFF_CALLBACK) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
PlaylistViewHolder.from(parent)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: PlaylistViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position), listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,10 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicMode>) :
|
||||||
icon = R.drawable.ic_genre_24
|
icon = R.drawable.ic_genre_24
|
||||||
string = R.string.lbl_genres
|
string = R.string.lbl_genres
|
||||||
}
|
}
|
||||||
|
MusicMode.PLAYLISTS -> {
|
||||||
|
icon = R.drawable.ic_playlist_24
|
||||||
|
string = R.string.lbl_playlists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use expected sw* size thresholds when choosing a configuration.
|
// Use expected sw* size thresholds when choosing a configuration.
|
||||||
|
|
|
@ -49,7 +49,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
//
|
//
|
||||||
// 0bTAB1_TAB2_TAB3_TAB4_TAB5
|
// 0bTAB1_TAB2_TAB3_TAB4_TAB5
|
||||||
//
|
//
|
||||||
// Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists.
|
// Where TABN is a chunk representing a tab at position N.
|
||||||
// Each chunk in a sequence is represented as:
|
// Each chunk in a sequence is represented as:
|
||||||
//
|
//
|
||||||
// VTTT
|
// VTTT
|
||||||
|
@ -57,18 +57,23 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
// Where V is a bit representing the visibility and T is a 3-bit integer representing the
|
// Where V is a bit representing the visibility and T is a 3-bit integer representing the
|
||||||
// MusicMode for this tab.
|
// MusicMode for this tab.
|
||||||
|
|
||||||
/** The length a well-formed tab sequence should be. */
|
/** The maximum index that a well-formed tab sequence should be. */
|
||||||
private const val SEQUENCE_LEN = 4
|
private const val MAX_SEQUENCE_IDX = 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default tab sequence, in integer form. This represents a set of four visible tabs
|
* The default tab sequence, in integer form. This represents a set of four visible tabs
|
||||||
* ordered as "Song", "Album", "Artist", and "Genre".
|
* ordered as "Song", "Album", "Artist", "Genre", and "Playlists
|
||||||
*/
|
*/
|
||||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_1100
|
||||||
|
|
||||||
/** Maps between the integer code in the tab sequence and it's [MusicMode]. */
|
/** Maps between the integer code in the tab sequence and it's [MusicMode]. */
|
||||||
private val MODE_TABLE =
|
private val MODE_TABLE =
|
||||||
arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
|
arrayOf(
|
||||||
|
MusicMode.SONGS,
|
||||||
|
MusicMode.ALBUMS,
|
||||||
|
MusicMode.ARTISTS,
|
||||||
|
MusicMode.GENRES,
|
||||||
|
MusicMode.PLAYLISTS)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an array of [Tab]s into it's integer representation.
|
* Convert an array of [Tab]s into it's integer representation.
|
||||||
|
@ -81,7 +86,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
val distinct = tabs.distinctBy { it.mode }
|
val distinct = tabs.distinctBy { it.mode }
|
||||||
|
|
||||||
var sequence = 0b0100
|
var sequence = 0b0100
|
||||||
var shift = SEQUENCE_LEN * 4
|
var shift = MAX_SEQUENCE_IDX * 4
|
||||||
for (tab in distinct) {
|
for (tab in distinct) {
|
||||||
val bin =
|
val bin =
|
||||||
when (tab) {
|
when (tab) {
|
||||||
|
@ -107,9 +112,8 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
|
|
||||||
// Try to parse a mode for each chunk in the sequence.
|
// Try to parse a mode for each chunk in the sequence.
|
||||||
// If we can't parse one, just skip it.
|
// If we can't parse one, just skip it.
|
||||||
for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) {
|
for (shift in (0..MAX_SEQUENCE_IDX * 4).reversed() step 4) {
|
||||||
val chunk = intCode.shr(shift) and 0b1111
|
val chunk = intCode.shr(shift) and 0b1111
|
||||||
|
|
||||||
val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue
|
val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue
|
||||||
|
|
||||||
// Figure out the visibility
|
// Figure out the visibility
|
||||||
|
@ -125,7 +129,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
val distinct = tabs.distinctBy { it.mode }
|
val distinct = tabs.distinctBy { it.mode }
|
||||||
|
|
||||||
// For safety, return null if we have an empty or larger-than-expected tab array.
|
// For safety, return null if we have an empty or larger-than-expected tab array.
|
||||||
if (distinct.isEmpty() || distinct.size < SEQUENCE_LEN) {
|
if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) {
|
||||||
logE("Sequence size was ${distinct.size}, which is invalid")
|
logE("Sequence size was ${distinct.size}, which is invalid")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
|
||||||
MusicMode.ALBUMS -> R.string.lbl_albums
|
MusicMode.ALBUMS -> R.string.lbl_albums
|
||||||
MusicMode.ARTISTS -> R.string.lbl_artists
|
MusicMode.ARTISTS -> R.string.lbl_artists
|
||||||
MusicMode.GENRES -> R.string.lbl_genres
|
MusicMode.GENRES -> R.string.lbl_genres
|
||||||
|
MusicMode.PLAYLISTS -> R.string.lbl_playlists
|
||||||
})
|
})
|
||||||
|
|
||||||
// Unlike in other adapters, we update the checked state alongside
|
// Unlike in other adapters, we update the checked state alongside
|
||||||
|
|
|
@ -30,10 +30,7 @@ import androidx.annotation.AttrRes
|
||||||
import androidx.core.view.updateMarginsRelative
|
import androidx.core.view.updateMarginsRelative
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.Artist
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDimenPixels
|
import org.oxycblt.auxio.util.getDimenPixels
|
||||||
|
@ -177,6 +174,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
*/
|
*/
|
||||||
fun bind(genre: Genre) = innerImageView.bind(genre)
|
fun bind(genre: Genre) = innerImageView.bind(genre)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a [Playlist]'s image to the internal [StyledImageView].
|
||||||
|
*
|
||||||
|
* @param playlist the [Playlist] to bind.
|
||||||
|
* @see StyledImageView.bind
|
||||||
|
*/
|
||||||
|
fun bind(playlist: Playlist) = innerImageView.bind(playlist)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this view should be indicated to have ongoing playback or not. See
|
* Whether this view should be indicated to have ongoing playback or not. See
|
||||||
* PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
|
* PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
|
||||||
|
|
|
@ -46,7 +46,8 @@ class CoilModule {
|
||||||
songFactory: AlbumCoverFetcher.SongFactory,
|
songFactory: AlbumCoverFetcher.SongFactory,
|
||||||
albumFactory: AlbumCoverFetcher.AlbumFactory,
|
albumFactory: AlbumCoverFetcher.AlbumFactory,
|
||||||
artistFactory: ArtistImageFetcher.Factory,
|
artistFactory: ArtistImageFetcher.Factory,
|
||||||
genreFactory: GenreImageFetcher.Factory
|
genreFactory: GenreImageFetcher.Factory,
|
||||||
|
playlistFactory: PlaylistImageFetcher.Factory
|
||||||
) =
|
) =
|
||||||
ImageLoader.Builder(context)
|
ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
|
@ -56,6 +57,7 @@ class CoilModule {
|
||||||
add(albumFactory)
|
add(albumFactory)
|
||||||
add(artistFactory)
|
add(artistFactory)
|
||||||
add(genreFactory)
|
add(genreFactory)
|
||||||
|
add(playlistFactory)
|
||||||
}
|
}
|
||||||
// Use our own crossfade with error drawable support
|
// Use our own crossfade with error drawable support
|
||||||
.transitionFactory(ErrorCrossfadeTransitionFactory())
|
.transitionFactory(ErrorCrossfadeTransitionFactory())
|
||||||
|
|
|
@ -38,11 +38,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
|
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
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.ui.UISettings
|
import org.oxycblt.auxio.ui.UISettings
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
|
@ -123,6 +119,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
*/
|
*/
|
||||||
fun bind(genre: Genre) = bindImpl(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
|
fun bind(genre: Genre) = bindImpl(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a [Playlist]'s image to this view, also updating the content description.
|
||||||
|
*
|
||||||
|
* @param playlist the [Playlist] to bind.
|
||||||
|
*/
|
||||||
|
fun bind(playlist: Playlist) =
|
||||||
|
bindImpl(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internally bind a [Music]'s image to this view.
|
* Internally bind a [Music]'s image to this view.
|
||||||
*
|
*
|
||||||
|
|
|
@ -33,11 +33,7 @@ import kotlin.math.min
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.Artist
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
|
||||||
import org.oxycblt.auxio.music.Music
|
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Keyer] implementation for [Music] data.
|
* A [Keyer] implementation for [Music] data.
|
||||||
|
@ -74,14 +70,12 @@ private constructor(
|
||||||
dataSource = DataSource.DISK)
|
dataSource = DataSource.DISK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A [Fetcher.Factory] implementation that works with [Song]s. */
|
|
||||||
class SongFactory @Inject constructor(private val coverExtractor: CoverExtractor) :
|
class SongFactory @Inject constructor(private val coverExtractor: CoverExtractor) :
|
||||||
Fetcher.Factory<Song> {
|
Fetcher.Factory<Song> {
|
||||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
|
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
|
||||||
AlbumCoverFetcher(options.context, coverExtractor, data.album)
|
AlbumCoverFetcher(options.context, coverExtractor, data.album)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A [Fetcher.Factory] implementation that works with [Album]s. */
|
|
||||||
class AlbumFactory @Inject constructor(private val coverExtractor: CoverExtractor) :
|
class AlbumFactory @Inject constructor(private val coverExtractor: CoverExtractor) :
|
||||||
Fetcher.Factory<Album> {
|
Fetcher.Factory<Album> {
|
||||||
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
|
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
|
||||||
|
@ -108,7 +102,6 @@ private constructor(
|
||||||
return Images.createMosaic(context, results, size)
|
return Images.createMosaic(context, results, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [Fetcher.Factory] implementation. */
|
|
||||||
class Factory @Inject constructor(private val extractor: CoverExtractor) :
|
class Factory @Inject constructor(private val extractor: CoverExtractor) :
|
||||||
Fetcher.Factory<Artist> {
|
Fetcher.Factory<Artist> {
|
||||||
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
|
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
|
||||||
|
@ -133,7 +126,6 @@ private constructor(
|
||||||
return Images.createMosaic(context, results, size)
|
return Images.createMosaic(context, results, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [Fetcher.Factory] implementation. */
|
|
||||||
class Factory @Inject constructor(private val extractor: CoverExtractor) :
|
class Factory @Inject constructor(private val extractor: CoverExtractor) :
|
||||||
Fetcher.Factory<Genre> {
|
Fetcher.Factory<Genre> {
|
||||||
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
|
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
|
||||||
|
@ -141,6 +133,30 @@ private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Fetcher] for [Playlist] images. Use [Factory] for instantiation.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class PlaylistImageFetcher
|
||||||
|
private constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val extractor: CoverExtractor,
|
||||||
|
private val size: Size,
|
||||||
|
private val playlist: Playlist
|
||||||
|
) : Fetcher {
|
||||||
|
override suspend fun fetch(): FetchResult? {
|
||||||
|
val results = playlist.albums.mapAtMostNotNull(4) { album -> extractor.extract(album) }
|
||||||
|
return Images.createMosaic(context, results, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory @Inject constructor(private val extractor: CoverExtractor) :
|
||||||
|
Fetcher.Factory<Playlist> {
|
||||||
|
override fun create(data: Playlist, options: Options, imageLoader: ImageLoader) =
|
||||||
|
PlaylistImageFetcher(options.context, extractor, options.size, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
|
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
|
||||||
* transformed into [R].
|
* transformed into [R].
|
||||||
|
|
|
@ -217,6 +217,40 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a menu in the context of a [Playlist]. This menu will be managed by the Fragment and
|
||||||
|
* closed when the view is destroyed. If a menu is already opened, this call is ignored.
|
||||||
|
*
|
||||||
|
* @param anchor The [View] to anchor the menu to.
|
||||||
|
* @param menuRes The resource of the menu to load.
|
||||||
|
* @param genre The [Playlist] to create the menu for.
|
||||||
|
*/
|
||||||
|
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Playlist) {
|
||||||
|
logD("Launching new genre menu: ${genre.rawName}")
|
||||||
|
|
||||||
|
openMusicMenuImpl(anchor, menuRes) {
|
||||||
|
when (it.itemId) {
|
||||||
|
R.id.action_play -> {
|
||||||
|
// playbackModel.play(genre)
|
||||||
|
}
|
||||||
|
R.id.action_shuffle -> {
|
||||||
|
// playbackModel.shuffle(genre)
|
||||||
|
}
|
||||||
|
R.id.action_play_next -> {
|
||||||
|
// playbackModel.playNext(genre)
|
||||||
|
// requireContext().showToast(R.string.lng_queue_added)
|
||||||
|
}
|
||||||
|
R.id.action_queue_add -> {
|
||||||
|
// playbackModel.addToQueue(genre)
|
||||||
|
// requireContext().showToast(R.string.lng_queue_added)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
error("Unexpected menu item selected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openMusicMenuImpl(
|
private fun openMusicMenuImpl(
|
||||||
anchor: View,
|
anchor: View,
|
||||||
@MenuRes menuRes: Int,
|
@MenuRes menuRes: Int,
|
||||||
|
|
|
@ -102,41 +102,37 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a *mutable* list of [Song]s in-place using this [Sort]'s configuration.
|
* Sort a list of [Playlist]s.
|
||||||
*
|
*
|
||||||
* @param songs The [Song]s to sort.
|
* @param playlists The list of [Playlist]s.
|
||||||
|
* @return A new list of [Playlist]s sorted by this [Sort]'s configuration
|
||||||
*/
|
*/
|
||||||
|
fun <T : Playlist> playlists(playlists: Collection<T>): List<T> {
|
||||||
|
val mutable = playlists.toMutableList()
|
||||||
|
playlistsInPlace(mutable)
|
||||||
|
return mutable
|
||||||
|
}
|
||||||
|
|
||||||
private fun songsInPlace(songs: MutableList<out Song>) {
|
private fun songsInPlace(songs: MutableList<out Song>) {
|
||||||
songs.sortWith(mode.getSongComparator(direction))
|
songs.sortWith(mode.getSongComparator(direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a *mutable* list of [Album]s in-place using this [Sort]'s configuration.
|
|
||||||
*
|
|
||||||
* @param albums The [Album]s to sort.
|
|
||||||
*/
|
|
||||||
private fun albumsInPlace(albums: MutableList<out Album>) {
|
private fun albumsInPlace(albums: MutableList<out Album>) {
|
||||||
albums.sortWith(mode.getAlbumComparator(direction))
|
albums.sortWith(mode.getAlbumComparator(direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a *mutable* list of [Artist]s in-place using this [Sort]'s configuration.
|
|
||||||
*
|
|
||||||
* @param artists The [Album]s to sort.
|
|
||||||
*/
|
|
||||||
private fun artistsInPlace(artists: MutableList<out Artist>) {
|
private fun artistsInPlace(artists: MutableList<out Artist>) {
|
||||||
artists.sortWith(mode.getArtistComparator(direction))
|
artists.sortWith(mode.getArtistComparator(direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a *mutable* list of [Genre]s in-place using this [Sort]'s configuration.
|
|
||||||
*
|
|
||||||
* @param genres The [Genre]s to sort.
|
|
||||||
*/
|
|
||||||
private fun genresInPlace(genres: MutableList<out Genre>) {
|
private fun genresInPlace(genres: MutableList<out Genre>) {
|
||||||
genres.sortWith(mode.getGenreComparator(direction))
|
genres.sortWith(mode.getGenreComparator(direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun playlistsInPlace(playlists: MutableList<out Playlist>) {
|
||||||
|
playlists.sortWith(mode.getPlaylistComparator(direction))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The integer representation of this instance.
|
* The integer representation of this instance.
|
||||||
*
|
*
|
||||||
|
@ -200,6 +196,16 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a [Comparator] that sorts [Playlist]s according to this [Mode].
|
||||||
|
*
|
||||||
|
* @param direction The direction to sort in.
|
||||||
|
* @return A [Comparator] that can be used to sort a [Genre] list according to this [Mode].
|
||||||
|
*/
|
||||||
|
open fun getPlaylistComparator(direction: Direction): Comparator<Playlist> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort by the item's name.
|
* Sort by the item's name.
|
||||||
*
|
*
|
||||||
|
@ -223,12 +229,15 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
|
|
||||||
override fun getGenreComparator(direction: Direction) =
|
override fun getGenreComparator(direction: Direction) =
|
||||||
compareByDynamic(direction, BasicComparator.GENRE)
|
compareByDynamic(direction, BasicComparator.GENRE)
|
||||||
|
|
||||||
|
override fun getPlaylistComparator(direction: Direction) =
|
||||||
|
compareByDynamic(direction, BasicComparator.PLAYLIST)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort by the [Album] of an item. Only available for [Song]s.
|
* Sort by the [Album] of an item. Only available for [Song]s.
|
||||||
*
|
*
|
||||||
* @see Album.collationKey
|
* @see Album.sortName
|
||||||
*/
|
*/
|
||||||
object ByAlbum : Mode() {
|
object ByAlbum : Mode() {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
|
@ -324,6 +333,11 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
override fun getGenreComparator(direction: Direction): Comparator<Genre> =
|
override fun getGenreComparator(direction: Direction): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(direction) { it.durationMs }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(direction) { it.durationMs }, compareBy(BasicComparator.GENRE))
|
||||||
|
|
||||||
|
override fun getPlaylistComparator(direction: Direction): Comparator<Playlist> =
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(direction) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.PLAYLIST))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -350,6 +364,11 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
override fun getGenreComparator(direction: Direction): Comparator<Genre> =
|
override fun getGenreComparator(direction: Direction): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(direction) { it.songs.size }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(direction) { it.songs.size }, compareBy(BasicComparator.GENRE))
|
||||||
|
|
||||||
|
override fun getPlaylistComparator(direction: Direction): Comparator<Playlist> =
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(direction) { it.songs.size },
|
||||||
|
compareBy(BasicComparator.PLAYLIST))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -555,6 +574,8 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
val ARTIST: Comparator<Artist> = BasicComparator()
|
val ARTIST: Comparator<Artist> = BasicComparator()
|
||||||
/** A re-usable instance configured for [Genre]s. */
|
/** A re-usable instance configured for [Genre]s. */
|
||||||
val GENRE: Comparator<Genre> = BasicComparator()
|
val GENRE: Comparator<Genre> = BasicComparator()
|
||||||
|
/** A re-usable instance configured for [Playlist]s. */
|
||||||
|
val PLAYLIST: Comparator<Playlist> = BasicComparator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -249,6 +249,60 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
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): Boolean =
|
||||||
|
oldItem.rawName == newItem.rawName &&
|
||||||
|
oldItem.artists.size == newItem.artists.size &&
|
||||||
|
oldItem.songs.size == newItem.songs.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecyclerView.ViewHolder] that displays a [Playlist]. Use [from] to create an instance.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class PlaylistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
|
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||||
|
/**
|
||||||
|
* Bind new data to this instance.
|
||||||
|
*
|
||||||
|
* @param playlist The new [Playlist] to bind.
|
||||||
|
* @param listener An [SelectableListListener] to bind interactions to.
|
||||||
|
*/
|
||||||
|
fun bind(playlist: Playlist, listener: SelectableListListener<Playlist>) {
|
||||||
|
listener.bind(playlist, this, menuButton = binding.parentMenu)
|
||||||
|
binding.parentImage.bind(playlist)
|
||||||
|
binding.parentName.text = playlist.resolveName(binding.context)
|
||||||
|
binding.parentInfo.text =
|
||||||
|
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
|
binding.root.isSelected = isActive
|
||||||
|
binding.parentImage.isPlaying = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
binding.root.isActivated = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Unique ID for this ViewHolder type. */
|
||||||
|
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_PLAYLIST
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
*
|
||||||
|
* @param parent The parent to inflate this instance from.
|
||||||
|
* @return A new instance.
|
||||||
|
*/
|
||||||
|
fun from(parent: View) =
|
||||||
|
PlaylistViewHolder(ItemParentBinding.inflate(parent.context.inflater))
|
||||||
|
|
||||||
|
/** A comparator that can be used with DiffUtil. */
|
||||||
|
val DIFF_CALLBACK =
|
||||||
|
object : SimpleDiffCallback<Playlist>() {
|
||||||
|
override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist): Boolean =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,7 @@ package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
||||||
/**
|
/** Version-aware permission identifier for reading audio files. */
|
||||||
* Version-aware permission identifier for reading audio files.
|
|
||||||
*/
|
|
||||||
val PERMISSION_READ_AUDIO =
|
val PERMISSION_READ_AUDIO =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
android.Manifest.permission.READ_MEDIA_AUDIO
|
android.Manifest.permission.READ_MEDIA_AUDIO
|
||||||
|
@ -32,25 +30,29 @@ val PERMISSION_READ_AUDIO =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the current state of the music loader.
|
* Represents the current state of the music loader.
|
||||||
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed interface IndexingState {
|
sealed interface IndexingState {
|
||||||
/**
|
/**
|
||||||
* Music loading is on-going.
|
* Music loading is on-going.
|
||||||
|
*
|
||||||
* @param progress The current progress of the music loading.
|
* @param progress The current progress of the music loading.
|
||||||
*/
|
*/
|
||||||
data class Indexing(val progress: IndexingProgress) : IndexingState
|
data class Indexing(val progress: IndexingProgress) : IndexingState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Music loading has completed.
|
* Music loading has completed.
|
||||||
* @param error If music loading has failed, the error that occurred will be here. Otherwise,
|
*
|
||||||
* it will be null.
|
* @param error If music loading has failed, the error that occurred will be here. Otherwise, it
|
||||||
|
* will be null.
|
||||||
*/
|
*/
|
||||||
data class Completed(val error: Throwable?) : IndexingState
|
data class Completed(val error: Throwable?) : IndexingState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the current progress of music loading.
|
* Represents the current progress of music loading.
|
||||||
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed interface IndexingProgress {
|
sealed interface IndexingProgress {
|
||||||
|
@ -59,6 +61,7 @@ sealed interface IndexingProgress {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Songs are currently being loaded.
|
* Songs are currently being loaded.
|
||||||
|
*
|
||||||
* @param current The current amount of songs loaded.
|
* @param current The current amount of songs loaded.
|
||||||
* @param total The projected total amount of songs.
|
* @param total The projected total amount of songs.
|
||||||
*/
|
*/
|
||||||
|
@ -67,6 +70,7 @@ sealed interface IndexingProgress {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown by the music loader when [PERMISSION_READ_AUDIO] was not granted.
|
* Thrown by the music loader when [PERMISSION_READ_AUDIO] was not granted.
|
||||||
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class NoAudioPermissionException : Exception() {
|
class NoAudioPermissionException : Exception() {
|
||||||
|
@ -75,6 +79,7 @@ class NoAudioPermissionException : Exception() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when no music was found.
|
* Thrown when no music was found.
|
||||||
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class NoMusicException : Exception() {
|
class NoMusicException : Exception() {
|
||||||
|
|
|
@ -370,7 +370,12 @@ interface Genre : MusicParent {
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface Playlist : MusicParent
|
interface Playlist : MusicParent {
|
||||||
|
/** The albums indirectly linked to by the [Song]s of this [Playlist]. */
|
||||||
|
val albums: List<Album>
|
||||||
|
/** The total duration of the songs in this genre, in milliseconds. */
|
||||||
|
val durationMs: Long
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A black-box datatype for a variation of music names that is suitable for music-oriented sorting.
|
* A black-box datatype for a variation of music names that is suitable for music-oriented sorting.
|
||||||
|
|
|
@ -33,7 +33,9 @@ enum class MusicMode {
|
||||||
/** Configure with respect to [Artist] instances. */
|
/** Configure with respect to [Artist] instances. */
|
||||||
ARTISTS,
|
ARTISTS,
|
||||||
/** Configure with respect to [Genre] instances. */
|
/** Configure with respect to [Genre] instances. */
|
||||||
GENRES;
|
GENRES,
|
||||||
|
/** Configure with respect to [Playlist] instances. */
|
||||||
|
PLAYLISTS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The integer representation of this instance.
|
* The integer representation of this instance.
|
||||||
|
@ -47,6 +49,7 @@ enum class MusicMode {
|
||||||
ALBUMS -> IntegerTable.MUSIC_MODE_ALBUMS
|
ALBUMS -> IntegerTable.MUSIC_MODE_ALBUMS
|
||||||
ARTISTS -> IntegerTable.MUSIC_MODE_ARTISTS
|
ARTISTS -> IntegerTable.MUSIC_MODE_ARTISTS
|
||||||
GENRES -> IntegerTable.MUSIC_MODE_GENRES
|
GENRES -> IntegerTable.MUSIC_MODE_GENRES
|
||||||
|
PLAYLISTS -> IntegerTable.MUSIC_MODE_PLAYLISTS
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -63,6 +66,7 @@ enum class MusicMode {
|
||||||
IntegerTable.MUSIC_MODE_ALBUMS -> ALBUMS
|
IntegerTable.MUSIC_MODE_ALBUMS -> ALBUMS
|
||||||
IntegerTable.MUSIC_MODE_ARTISTS -> ARTISTS
|
IntegerTable.MUSIC_MODE_ARTISTS -> ARTISTS
|
||||||
IntegerTable.MUSIC_MODE_GENRES -> GENRES
|
IntegerTable.MUSIC_MODE_GENRES -> GENRES
|
||||||
|
IntegerTable.MUSIC_MODE_PLAYLISTS -> PLAYLISTS
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
/**
|
/**
|
||||||
* Primary manager of music information and loading.
|
* Primary manager of music information and loading.
|
||||||
*
|
*
|
||||||
* Music information is loaded in-memory by this repository using an [IndexingWorker].
|
* Music information is loaded in-memory by this repository using an [IndexingWorker]. Changes in
|
||||||
* Changes in music (loading) can be reacted to with [UpdateListener] and [IndexingListener].
|
* music (loading) can be reacted to with [UpdateListener] and [IndexingListener].
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -52,6 +52,7 @@ interface MusicRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an [UpdateListener] to receive updates from this instance.
|
* Add an [UpdateListener] to receive updates from this instance.
|
||||||
|
*
|
||||||
* @param listener The [UpdateListener] to add.
|
* @param listener The [UpdateListener] to add.
|
||||||
*/
|
*/
|
||||||
fun addUpdateListener(listener: UpdateListener)
|
fun addUpdateListener(listener: UpdateListener)
|
||||||
|
@ -59,12 +60,14 @@ interface MusicRepository {
|
||||||
/**
|
/**
|
||||||
* Remove an [UpdateListener] such that it does not receive any further updates from this
|
* Remove an [UpdateListener] such that it does not receive any further updates from this
|
||||||
* instance.
|
* instance.
|
||||||
|
*
|
||||||
* @param listener The [UpdateListener] to remove.
|
* @param listener The [UpdateListener] to remove.
|
||||||
*/
|
*/
|
||||||
fun removeUpdateListener(listener: UpdateListener)
|
fun removeUpdateListener(listener: UpdateListener)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an [IndexingListener] to receive updates from this instance.
|
* Add an [IndexingListener] to receive updates from this instance.
|
||||||
|
*
|
||||||
* @param listener The [UpdateListener] to add.
|
* @param listener The [UpdateListener] to add.
|
||||||
*/
|
*/
|
||||||
fun addIndexingListener(listener: IndexingListener)
|
fun addIndexingListener(listener: IndexingListener)
|
||||||
|
@ -72,6 +75,7 @@ interface MusicRepository {
|
||||||
/**
|
/**
|
||||||
* Remove an [IndexingListener] such that it does not receive any further updates from this
|
* Remove an [IndexingListener] such that it does not receive any further updates from this
|
||||||
* instance.
|
* instance.
|
||||||
|
*
|
||||||
* @param listener The [IndexingListener] to remove.
|
* @param listener The [IndexingListener] to remove.
|
||||||
*/
|
*/
|
||||||
fun removeIndexingListener(listener: IndexingListener)
|
fun removeIndexingListener(listener: IndexingListener)
|
||||||
|
@ -79,13 +83,15 @@ interface MusicRepository {
|
||||||
/**
|
/**
|
||||||
* Register an [IndexingWorker] to handle loading operations. Will do nothing if one is already
|
* Register an [IndexingWorker] to handle loading operations. Will do nothing if one is already
|
||||||
* registered.
|
* registered.
|
||||||
|
*
|
||||||
* @param worker The [IndexingWorker] to register.
|
* @param worker The [IndexingWorker] to register.
|
||||||
*/
|
*/
|
||||||
fun registerWorker(worker: IndexingWorker)
|
fun registerWorker(worker: IndexingWorker)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister an [IndexingWorker] and drop any work currently being done by it. Does nothing
|
* Unregister an [IndexingWorker] and drop any work currently being done by it. Does nothing if
|
||||||
* if given [IndexingWorker] is not the currently registered instance.
|
* given [IndexingWorker] is not the currently registered instance.
|
||||||
|
*
|
||||||
* @param worker The [IndexingWorker] to unregister.
|
* @param worker The [IndexingWorker] to unregister.
|
||||||
*/
|
*/
|
||||||
fun unregisterWorker(worker: IndexingWorker)
|
fun unregisterWorker(worker: IndexingWorker)
|
||||||
|
@ -93,62 +99,56 @@ interface MusicRepository {
|
||||||
/**
|
/**
|
||||||
* Request that a music loading operation is started by the current [IndexingWorker]. Does
|
* Request that a music loading operation is started by the current [IndexingWorker]. Does
|
||||||
* nothing if one is not available.
|
* nothing if one is not available.
|
||||||
|
*
|
||||||
* @param withCache Whether to load with the music cache or not.
|
* @param withCache Whether to load with the music cache or not.
|
||||||
*/
|
*/
|
||||||
fun requestIndex(withCache: Boolean)
|
fun requestIndex(withCache: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the music library. Any prior loads will be canceled.
|
* Load the music library. Any prior loads will be canceled.
|
||||||
|
*
|
||||||
* @param worker The [IndexingWorker] to perform the work with.
|
* @param worker The [IndexingWorker] to perform the work with.
|
||||||
* @param withCache Whether to load with the music cache or not.
|
* @param withCache Whether to load with the music cache or not.
|
||||||
* @return The top-level music loading [Job] started.
|
* @return The top-level music loading [Job] started.
|
||||||
*/
|
*/
|
||||||
fun index(worker: IndexingWorker, withCache: Boolean): Job
|
fun index(worker: IndexingWorker, withCache: Boolean): Job
|
||||||
|
|
||||||
/**
|
/** A listener for changes to the stored music information. */
|
||||||
* A listener for changes to the stored music information.
|
|
||||||
*/
|
|
||||||
interface UpdateListener {
|
interface UpdateListener {
|
||||||
/**
|
/**
|
||||||
* Called when a change to the stored music information occurs.
|
* Called when a change to the stored music information occurs.
|
||||||
|
*
|
||||||
* @param changes The [Changes] that have occured.
|
* @param changes The [Changes] that have occured.
|
||||||
*/
|
*/
|
||||||
fun onMusicChanges(changes: Changes)
|
fun onMusicChanges(changes: Changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags indicating which kinds of music information changed.
|
* Flags indicating which kinds of music information changed.
|
||||||
|
*
|
||||||
* @param library Whether the current [Library] has changed.
|
* @param library Whether the current [Library] has changed.
|
||||||
* @param playlists Whether the current [Playlist]s have changed.
|
* @param playlists Whether the current [Playlist]s have changed.
|
||||||
*/
|
*/
|
||||||
data class Changes(val library: Boolean, val playlists: Boolean)
|
data class Changes(val library: Boolean, val playlists: Boolean)
|
||||||
|
|
||||||
/**
|
/** A listener for events in the music loading process. */
|
||||||
* A listener for events in the music loading process.
|
|
||||||
*/
|
|
||||||
interface IndexingListener {
|
interface IndexingListener {
|
||||||
/**
|
/** Called when the music loading state changed. */
|
||||||
* Called when the music loading state changed.
|
|
||||||
*/
|
|
||||||
fun onIndexingStateChanged()
|
fun onIndexingStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A persistent worker that can load music in the background. */
|
||||||
* A persistent worker that can load music in the background.
|
|
||||||
*/
|
|
||||||
interface IndexingWorker {
|
interface IndexingWorker {
|
||||||
/**
|
/** A [Context] required to read device storage */
|
||||||
* A [Context] required to read device storage
|
|
||||||
*/
|
|
||||||
val context: Context
|
val context: Context
|
||||||
|
|
||||||
/**
|
/** The [CoroutineScope] to perform coroutine music loading work on. */
|
||||||
* The [CoroutineScope] to perform coroutine music loading work on.
|
|
||||||
*/
|
|
||||||
val scope: CoroutineScope
|
val scope: CoroutineScope
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that the music loading process ([index]) should be started. Any prior
|
* Request that the music loading process ([index]) should be started. Any prior loads
|
||||||
* loads should be canceled.
|
* should be canceled.
|
||||||
|
*
|
||||||
* @param withCache Whether to use the music cache when loading.
|
* @param withCache Whether to use the music cache when loading.
|
||||||
*/
|
*/
|
||||||
fun requestIndex(withCache: Boolean)
|
fun requestIndex(withCache: Boolean)
|
||||||
|
@ -301,6 +301,7 @@ constructor(
|
||||||
cacheRepository.writeCache(rawSongs)
|
cacheRepository.writeCache(rawSongs)
|
||||||
}
|
}
|
||||||
val newLibrary = libraryJob.await()
|
val newLibrary = libraryJob.await()
|
||||||
|
// TODO: Make real playlist reading
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
emitComplete(null)
|
emitComplete(null)
|
||||||
emitData(newLibrary, listOf())
|
emitData(newLibrary, listOf())
|
||||||
|
|
|
@ -54,6 +54,8 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
||||||
var artistSort: Sort
|
var artistSort: Sort
|
||||||
/** The [Sort] mode used in [Genre] lists. */
|
/** The [Sort] mode used in [Genre] lists. */
|
||||||
var genreSort: Sort
|
var genreSort: Sort
|
||||||
|
/** The [Sort] mode used in [Playlist] lists. */
|
||||||
|
var playlistSort: Sort
|
||||||
/** The [Sort] mode used in an [Album]'s [Song] list. */
|
/** The [Sort] mode used in an [Album]'s [Song] list. */
|
||||||
var albumSongSort: Sort
|
var albumSongSort: Sort
|
||||||
/** The [Sort] mode used in an [Artist]'s [Song] list. */
|
/** The [Sort] mode used in an [Artist]'s [Song] list. */
|
||||||
|
@ -161,6 +163,17 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var playlistSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_playlists_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_playlists_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
override var albumSongSort: Sort
|
override var albumSongSort: Sort
|
||||||
get() {
|
get() {
|
||||||
var sort =
|
var sort =
|
||||||
|
|
|
@ -30,4 +30,7 @@ class PlaylistImpl(rawPlaylist: RawPlaylist, library: Library, musicSettings: Mu
|
||||||
override val rawSortName = null
|
override val rawSortName = null
|
||||||
override val sortName = SortName(rawName, musicSettings)
|
override val sortName = SortName(rawName, musicSettings)
|
||||||
override val songs = rawPlaylist.songs.mapNotNull { library.find<Song>(it.songUid) }
|
override val songs = rawPlaylist.songs.mapNotNull { library.find<Song>(it.songUid) }
|
||||||
|
override val durationMs = songs.sumOf { it.durationMs }
|
||||||
|
override val albums =
|
||||||
|
songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,7 @@ constructor(
|
||||||
MusicMode.ALBUMS -> playImpl(song, song.album)
|
MusicMode.ALBUMS -> playImpl(song, song.album)
|
||||||
MusicMode.ARTISTS -> playFromArtist(song)
|
MusicMode.ARTISTS -> playFromArtist(song)
|
||||||
MusicMode.GENRES -> playFromGenre(song)
|
MusicMode.GENRES -> playFromGenre(song)
|
||||||
|
MusicMode.PLAYLISTS -> error("Playing from a playlist is not supported.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -148,9 +148,9 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||||
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||||
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
is Genre -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
is Genre -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
is Playlist -> TODO("handle this")
|
is Playlist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,7 @@ constructor(
|
||||||
MusicMode.ALBUMS -> R.id.option_filter_albums
|
MusicMode.ALBUMS -> R.id.option_filter_albums
|
||||||
MusicMode.ARTISTS -> R.id.option_filter_artists
|
MusicMode.ARTISTS -> R.id.option_filter_artists
|
||||||
MusicMode.GENRES -> R.id.option_filter_genres
|
MusicMode.GENRES -> R.id.option_filter_genres
|
||||||
|
MusicMode.PLAYLISTS -> R.id.option_filter_all // TODO: Handle
|
||||||
// Null maps to filtering nothing.
|
// Null maps to filtering nothing.
|
||||||
null -> R.id.option_filter_all
|
null -> R.id.option_filter_all
|
||||||
}
|
}
|
||||||
|
|
11
app/src/main/res/drawable/ic_playlist_24.xml
Normal file
11
app/src/main/res/drawable/ic_playlist_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M2.5,16V14H10.5V16ZM2.5,12V10H14.5V12ZM2.5,8V6H14.5V8ZM15.5,21V13L21.5,17Z"/>
|
||||||
|
</vector>
|
|
@ -5,6 +5,7 @@
|
||||||
<item name="home_album_recycler" type="id" />
|
<item name="home_album_recycler" type="id" />
|
||||||
<item name="home_artist_recycler" type="id" />
|
<item name="home_artist_recycler" type="id" />
|
||||||
<item name="home_genre_recycler" type="id" />
|
<item name="home_genre_recycler" type="id" />
|
||||||
|
<item name="home_playlist_recycler" type="id" />
|
||||||
|
|
||||||
<integer name="anim_fade_enter_duration">200</integer>
|
<integer name="anim_fade_enter_duration">200</integer>
|
||||||
<integer name="anim_fade_exit_duration">100</integer>
|
<integer name="anim_fade_exit_duration">100</integer>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="fmt_number" translatable="false">%d</string>
|
<string name="fmt_number" translatable="false">%d</string>
|
||||||
<string name="fmt_zipped_names" translatable="false">%1$s (%2$s)</string>
|
<string name="fmt_zipped_names" translatable="false">%1$s (%2$s)</string>
|
||||||
<string name="fmt_date_range" translatable="false">%s - %s</string>
|
<string name="fmt_date_range" translatable="false">%s - %s</string>
|
||||||
<string name="fmt_path">%1$s/%2$s</string>
|
<string name="fmt_path" translatable="false">%1$s/%2$s</string>
|
||||||
|
|
||||||
<!-- Codec Namespace | Format names -->
|
<!-- Codec Namespace | Format names -->
|
||||||
<string name="cdc_vorbis">Vorbis</string>
|
<string name="cdc_vorbis">Vorbis</string>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<string name="set_key_wipe_state" translatable="false">auxio_wipe_state</string>
|
<string name="set_key_wipe_state" translatable="false">auxio_wipe_state</string>
|
||||||
<string name="set_key_restore_state" translatable="false">auxio_restore_state</string>
|
<string name="set_key_restore_state" translatable="false">auxio_restore_state</string>
|
||||||
|
|
||||||
<string name="set_key_home_tabs" translatable="false">auxio_lib_tabs</string>
|
<string name="set_key_home_tabs" translatable="false">auxio_home_tabs</string>
|
||||||
<string name="set_key_hide_collaborators" translatable="false">auxio_hide_collaborators</string>
|
<string name="set_key_hide_collaborators" translatable="false">auxio_hide_collaborators</string>
|
||||||
<string name="set_key_round_mode" translatable="false">auxio_round_covers</string>
|
<string name="set_key_round_mode" translatable="false">auxio_round_covers</string>
|
||||||
<string name="set_key_bar_action" translatable="false">auxio_bar_action</string>
|
<string name="set_key_bar_action" translatable="false">auxio_bar_action</string>
|
||||||
|
@ -47,6 +47,7 @@
|
||||||
<string name="set_key_albums_sort" translatable="false">auxio_albums_sort</string>
|
<string name="set_key_albums_sort" translatable="false">auxio_albums_sort</string>
|
||||||
<string name="set_key_artists_sort" translatable="false">auxio_artists_sort</string>
|
<string name="set_key_artists_sort" translatable="false">auxio_artists_sort</string>
|
||||||
<string name="set_key_genres_sort" translatable="false">auxio_genres_sort</string>
|
<string name="set_key_genres_sort" translatable="false">auxio_genres_sort</string>
|
||||||
|
<string name="set_key_playlists_sort" translatable="false">auxio_playlists_sort</string>
|
||||||
|
|
||||||
<string name="set_key_album_songs_sort" translatable="false">auxio_album_sort</string>
|
<string name="set_key_album_songs_sort" translatable="false">auxio_album_sort</string>
|
||||||
<string name="set_key_artist_songs_sort" translatable="false">auxio_artist_sort</string>
|
<string name="set_key_artist_songs_sort" translatable="false">auxio_artist_sort</string>
|
||||||
|
|
|
@ -76,6 +76,9 @@
|
||||||
<string name="lbl_genre">Genre</string>
|
<string name="lbl_genre">Genre</string>
|
||||||
<string name="lbl_genres">Genres</string>
|
<string name="lbl_genres">Genres</string>
|
||||||
|
|
||||||
|
<string name="lbl_playlist">Playlist</string>
|
||||||
|
<string name="lbl_playlists">Playlists</string>
|
||||||
|
|
||||||
<!-- Search for music -->
|
<!-- Search for music -->
|
||||||
<string name="lbl_search">Search</string>
|
<string name="lbl_search">Search</string>
|
||||||
<!-- As in filtering to particular types of music in the search view -->
|
<!-- As in filtering to particular types of music in the search view -->
|
||||||
|
@ -307,6 +310,7 @@
|
||||||
<string name="desc_album_cover">Album cover for %s</string>
|
<string name="desc_album_cover">Album cover for %s</string>
|
||||||
<string name="desc_artist_image">Artist image for %s</string>
|
<string name="desc_artist_image">Artist image for %s</string>
|
||||||
<string name="desc_genre_image">Genre image for %s</string>
|
<string name="desc_genre_image">Genre image for %s</string>
|
||||||
|
<string name="desc_playlist_image">Playlist image for %s</string>
|
||||||
|
|
||||||
<!-- Default Namespace | Placeholder values -->
|
<!-- Default Namespace | Placeholder values -->
|
||||||
<eat-comment />
|
<eat-comment />
|
||||||
|
|
Loading…
Reference in a new issue