diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index d2d1e933f..47a4d9781 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -33,18 +33,20 @@ object IntegerTable { const val VIEW_TYPE_ARTIST = 0xA002 /** GenreViewHolder */ const val VIEW_TYPE_GENRE = 0xA003 + /** PlaylistViewHolder */ + const val VIEW_TYPE_PLAYLIST = 0xA004 /** BasicHeaderViewHolder */ - const val VIEW_TYPE_BASIC_HEADER = 0xA004 + const val VIEW_TYPE_BASIC_HEADER = 0xA005 /** SortHeaderViewHolder */ - const val VIEW_TYPE_SORT_HEADER = 0xA005 + const val VIEW_TYPE_SORT_HEADER = 0xA006 /** AlbumSongViewHolder */ const val VIEW_TYPE_ALBUM_SONG = 0xA007 /** ArtistAlbumViewHolder */ - const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 + const val VIEW_TYPE_ARTIST_ALBUM = 0xA008 /** ArtistSongViewHolder */ - const val VIEW_TYPE_ARTIST_SONG = 0xA00A + const val VIEW_TYPE_ARTIST_SONG = 0xA009 /** DiscHeaderViewHolder */ - const val VIEW_TYPE_DISC_HEADER = 0xA00C + const val VIEW_TYPE_DISC_HEADER = 0xA00A /** "Music playback" notification code */ const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 /** "Music loading" notification code */ @@ -65,16 +67,16 @@ object IntegerTable { const val PLAYBACK_MODE_IN_ALBUM = 0xA105 /** PlaybackMode.ALL_SONGS */ 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 */ 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 */ const val SORT_BY_NAME = 0xA10C /** Sort.ByArtist */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 78a2ff97d..d280a1e49 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -149,7 +149,7 @@ class GenreDetailFragment : override fun onOpenMenu(item: Music, anchor: View) { 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) else -> error("Unexpected datatype: ${item::class.simpleName}") } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 0b1bb4824..8b7e9abae 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -46,10 +46,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeBinding -import org.oxycblt.auxio.home.list.AlbumListFragment -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.list.* import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionFragment @@ -278,16 +275,8 @@ class HomeFragment : MusicMode.SONGS -> { id -> id != R.id.option_sort_count } // Disallow sorting by album for albums MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album } - // Only allow sorting by name, count, and duration for artists - MusicMode.ARTISTS -> { 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 -> + // Only allow sorting by name, count, and duration for parents + else -> { id -> id == R.id.option_sort_asc || id == R.id.option_sort_dec || id == R.id.option_sort_name || @@ -325,6 +314,7 @@ class HomeFragment : MusicMode.ALBUMS -> R.id.home_album_recycler MusicMode.ARTISTS -> R.id.home_artist_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.ARTISTS -> ArtistListFragment() MusicMode.GENRES -> GenreListFragment() + MusicMode.PLAYLISTS -> PlaylistListFragment() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index 776b0b219..53fa86faa 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -24,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab +import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.unlikelyToBeNull @@ -64,10 +65,29 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override val shouldHideCollaborators: Boolean 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) { when (key) { getString(R.string.set_key_home_tabs) -> listener.onTabsChanged() getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged() } } + + companion object { + const val OLD_KEY_LIB_TABS = "auxio_lib_tabs" + } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 081343cdb..d4a6c1c9c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -87,6 +87,15 @@ constructor( val genresInstructions: Event get() = _genresInstructions + private val _playlistsList = MutableStateFlow(listOf()) + /** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */ + val playlistsList: StateFlow> + get() = _playlistsList + private val _playlistsInstructions = MutableEvent() + /** Instructions for how to update [genresList] in the UI. */ + val playlistsInstructions: Event + get() = _playlistsInstructions + /** The [MusicMode] to use when playing a [Song] from the UI. */ val playbackMode: MusicMode get() = playbackSettings.inListPlaybackMode @@ -127,26 +136,34 @@ constructor( } override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.library) return - val library = musicRepository.library ?: return - logD("Library changed, refreshing library") - // Get the each list of items in the library to use as our list data. - // Applying the preferred sorting to them. - _songsInstructions.put(UpdateInstructions.Diff) - _songsList.value = musicSettings.songSort.songs(library.songs) - _albumsInstructions.put(UpdateInstructions.Diff) - _albumsLists.value = musicSettings.albumSort.albums(library.albums) - _artistsInstructions.put(UpdateInstructions.Diff) - _artistsList.value = - musicSettings.artistSort.artists( - if (homeSettings.shouldHideCollaborators) { - // Hide Collaborators is enabled, filter out collaborators. - library.artists.filter { !it.isCollaborator } - } else { - library.artists - }) - _genresInstructions.put(UpdateInstructions.Diff) - _genresList.value = musicSettings.genreSort.genres(library.genres) + val library = musicRepository.library + if (changes.library && library != null) { + logD("Refreshing library") + // Get the each list of items in the library to use as our list data. + // Applying the preferred sorting to them. + _songsInstructions.put(UpdateInstructions.Diff) + _songsList.value = musicSettings.songSort.songs(library.songs) + _albumsInstructions.put(UpdateInstructions.Diff) + _albumsLists.value = musicSettings.albumSort.albums(library.albums) + _artistsInstructions.put(UpdateInstructions.Diff) + _artistsList.value = + musicSettings.artistSort.artists( + if (homeSettings.shouldHideCollaborators) { + // Hide Collaborators is enabled, filter out collaborators. + library.artists.filter { !it.isCollaborator } + } else { + library.artists + }) + _genresInstructions.put(UpdateInstructions.Diff) + _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() { @@ -173,6 +190,7 @@ constructor( MusicMode.ALBUMS -> musicSettings.albumSort MusicMode.ARTISTS -> musicSettings.artistSort MusicMode.GENRES -> musicSettings.genreSort + MusicMode.PLAYLISTS -> musicSettings.playlistSort } /** @@ -204,6 +222,11 @@ constructor( _genresInstructions.put(UpdateInstructions.Replace(0)) _genresList.value = sort.genres(_genresList.value) } + MusicMode.PLAYLISTS -> { + musicSettings.playlistSort = sort + _playlistsInstructions.put(UpdateInstructions.Replace(0)) + _playlistsList.value = sort.playlists(_playlistsList.value) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index c6a58f594..29e490dc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -115,7 +115,7 @@ class ArtistListFragment : } 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 3561abbb4..f9b1f9aba 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -114,7 +114,7 @@ class GenreListFragment : } 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt new file mode 100644 index 000000000..12d21c7c9 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -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 . + */ + +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(), + 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) { + playlistAdapter.update( + playlists, homeModel.playlistsInstructions.consume().also { logD(it) }) + } + + private fun updateSelection(selection: List) { + 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) : + SelectionIndicatorAdapter(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) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt index e39c4a90f..718c99855 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt @@ -58,6 +58,10 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List) : icon = R.drawable.ic_genre_24 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. diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index e4aeb5d57..798d1888d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -49,7 +49,7 @@ sealed class Tab(open val mode: MusicMode) { // // 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: // // 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 // MusicMode for this tab. - /** The length a well-formed tab sequence should be. */ - private const val SEQUENCE_LEN = 4 + /** The maximum index that a well-formed tab sequence should be. */ + private const val MAX_SEQUENCE_IDX = 4 /** * 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]. */ 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. @@ -81,7 +86,7 @@ sealed class Tab(open val mode: MusicMode) { val distinct = tabs.distinctBy { it.mode } var sequence = 0b0100 - var shift = SEQUENCE_LEN * 4 + var shift = MAX_SEQUENCE_IDX * 4 for (tab in distinct) { val bin = when (tab) { @@ -107,9 +112,8 @@ sealed class Tab(open val mode: MusicMode) { // Try to parse a mode for each chunk in the sequence. // 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 mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue // Figure out the visibility @@ -125,7 +129,7 @@ sealed class Tab(open val mode: MusicMode) { val distinct = tabs.distinctBy { it.mode } // 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") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index de754bba9..a1b9db7fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -110,6 +110,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : MusicMode.ALBUMS -> R.string.lbl_albums MusicMode.ARTISTS -> R.string.lbl_artists MusicMode.GENRES -> R.string.lbl_genres + MusicMode.PLAYLISTS -> R.string.lbl_playlists }) // Unlike in other adapters, we update the checked state alongside diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 550f805e3..3f8652a7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -30,10 +30,7 @@ import androidx.annotation.AttrRes import androidx.core.view.updateMarginsRelative import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat 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) + /** + * 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 * PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt index ac9dd75c9..c73af2c73 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt @@ -46,7 +46,8 @@ class CoilModule { songFactory: AlbumCoverFetcher.SongFactory, albumFactory: AlbumCoverFetcher.AlbumFactory, artistFactory: ArtistImageFetcher.Factory, - genreFactory: GenreImageFetcher.Factory + genreFactory: GenreImageFetcher.Factory, + playlistFactory: PlaylistImageFetcher.Factory ) = ImageLoader.Builder(context) .components { @@ -56,6 +57,7 @@ class CoilModule { add(albumFactory) add(artistFactory) add(genreFactory) + add(playlistFactory) } // Use our own crossfade with error drawable support .transitionFactory(ErrorCrossfadeTransitionFactory()) diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 85ea9c730..8523ae18b 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -38,11 +38,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.image.extractor.SquareFrameTransform -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getColorCompat 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) + /** + * 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. * diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 8c9ff2e56..f2898f526 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -33,11 +33,7 @@ import kotlin.math.min import okio.buffer import okio.source import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.* /** * A [Keyer] implementation for [Music] data. @@ -74,14 +70,12 @@ private constructor( dataSource = DataSource.DISK) } - /** A [Fetcher.Factory] implementation that works with [Song]s. */ class SongFactory @Inject constructor(private val coverExtractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Song, options: Options, imageLoader: ImageLoader) = AlbumCoverFetcher(options.context, coverExtractor, data.album) } - /** A [Fetcher.Factory] implementation that works with [Album]s. */ class AlbumFactory @Inject constructor(private val coverExtractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Album, options: Options, imageLoader: ImageLoader) = @@ -108,7 +102,6 @@ private constructor( return Images.createMosaic(context, results, size) } - /** [Fetcher.Factory] implementation. */ class Factory @Inject constructor(private val extractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Artist, options: Options, imageLoader: ImageLoader) = @@ -133,7 +126,6 @@ private constructor( return Images.createMosaic(context, results, size) } - /** [Fetcher.Factory] implementation. */ class Factory @Inject constructor(private val extractor: CoverExtractor) : Fetcher.Factory { 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 { + 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 * transformed into [R]. diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index dc2393772..c3fc735b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -217,6 +217,40 @@ abstract class ListFragment : } } + /** + * 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( anchor: View, @MenuRes menuRes: Int, diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index ec64cdb3c..0aed7203c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -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 playlists(playlists: Collection): List { + val mutable = playlists.toMutableList() + playlistsInPlace(mutable) + return mutable + } + private fun songsInPlace(songs: MutableList) { 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) { 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) { 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) { genres.sortWith(mode.getGenreComparator(direction)) } + private fun playlistsInPlace(playlists: MutableList) { + playlists.sortWith(mode.getPlaylistComparator(direction)) + } + /** * The integer representation of this instance. * @@ -200,6 +196,16 @@ data class Sort(val mode: Mode, val direction: Direction) { 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 { + throw UnsupportedOperationException() + } + /** * Sort by the item's name. * @@ -223,12 +229,15 @@ data class Sort(val mode: Mode, val direction: Direction) { override fun getGenreComparator(direction: Direction) = 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. * - * @see Album.collationKey + * @see Album.sortName */ object ByAlbum : Mode() { override val intCode: Int @@ -324,6 +333,11 @@ data class Sort(val mode: Mode, val direction: Direction) { override fun getGenreComparator(direction: Direction): Comparator = MultiComparator( compareByDynamic(direction) { it.durationMs }, compareBy(BasicComparator.GENRE)) + + override fun getPlaylistComparator(direction: Direction): Comparator = + 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 = MultiComparator( compareByDynamic(direction) { it.songs.size }, compareBy(BasicComparator.GENRE)) + + override fun getPlaylistComparator(direction: Direction): Comparator = + 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 = BasicComparator() /** A re-usable instance configured for [Genre]s. */ val GENRE: Comparator = BasicComparator() + /** A re-usable instance configured for [Playlist]s. */ + val PLAYLIST: Comparator = BasicComparator() } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 1f5188c4f..7526ad0c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -249,6 +249,60 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding val DIFF_CALLBACK = object : SimpleDiffCallback() { 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) { + 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() { + override fun areContentsTheSame(oldItem: Playlist, newItem: Playlist): Boolean = oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt index 7741136a8..545a5f234 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt @@ -20,9 +20,7 @@ package org.oxycblt.auxio.music import android.os.Build -/** - * Version-aware permission identifier for reading audio files. - */ +/** Version-aware permission identifier for reading audio files. */ val PERMISSION_READ_AUDIO = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { android.Manifest.permission.READ_MEDIA_AUDIO @@ -32,25 +30,29 @@ val PERMISSION_READ_AUDIO = /** * Represents the current state of the music loader. + * * @author Alexander Capehart (OxygenCobalt) */ sealed interface IndexingState { /** * Music loading is on-going. + * * @param progress The current progress of the music loading. */ data class Indexing(val progress: IndexingProgress) : IndexingState /** * 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 } /** * Represents the current progress of music loading. + * * @author Alexander Capehart (OxygenCobalt) */ sealed interface IndexingProgress { @@ -59,6 +61,7 @@ sealed interface IndexingProgress { /** * Songs are currently being loaded. + * * @param current The current amount of songs loaded. * @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. + * * @author Alexander Capehart (OxygenCobalt) */ class NoAudioPermissionException : Exception() { @@ -75,6 +79,7 @@ class NoAudioPermissionException : Exception() { /** * Thrown when no music was found. + * * @author Alexander Capehart (OxygenCobalt) */ class NoMusicException : Exception() { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index e4b825201..f0f32a8b2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -370,7 +370,12 @@ interface Genre : MusicParent { * * @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 + /** 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. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt index f959e5f7d..03ec48dae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt @@ -33,7 +33,9 @@ enum class MusicMode { /** Configure with respect to [Artist] instances. */ ARTISTS, /** Configure with respect to [Genre] instances. */ - GENRES; + GENRES, + /** Configure with respect to [Playlist] instances. */ + PLAYLISTS; /** * The integer representation of this instance. @@ -47,6 +49,7 @@ enum class MusicMode { ALBUMS -> IntegerTable.MUSIC_MODE_ALBUMS ARTISTS -> IntegerTable.MUSIC_MODE_ARTISTS GENRES -> IntegerTable.MUSIC_MODE_GENRES + PLAYLISTS -> IntegerTable.MUSIC_MODE_PLAYLISTS } companion object { @@ -63,6 +66,7 @@ enum class MusicMode { IntegerTable.MUSIC_MODE_ALBUMS -> ALBUMS IntegerTable.MUSIC_MODE_ARTISTS -> ARTISTS IntegerTable.MUSIC_MODE_GENRES -> GENRES + IntegerTable.MUSIC_MODE_PLAYLISTS -> PLAYLISTS else -> null } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 07bbe4214..e6517d781 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.logW /** * Primary manager of music information and loading. * - * Music information is loaded in-memory by this repository using an [IndexingWorker]. - * Changes in music (loading) can be reacted to with [UpdateListener] and [IndexingListener]. + * Music information is loaded in-memory by this repository using an [IndexingWorker]. Changes in + * music (loading) can be reacted to with [UpdateListener] and [IndexingListener]. * * @author Alexander Capehart (OxygenCobalt) */ @@ -52,6 +52,7 @@ interface MusicRepository { /** * Add an [UpdateListener] to receive updates from this instance. + * * @param listener The [UpdateListener] to add. */ fun addUpdateListener(listener: UpdateListener) @@ -59,12 +60,14 @@ interface MusicRepository { /** * Remove an [UpdateListener] such that it does not receive any further updates from this * instance. + * * @param listener The [UpdateListener] to remove. */ fun removeUpdateListener(listener: UpdateListener) /** * Add an [IndexingListener] to receive updates from this instance. + * * @param listener The [UpdateListener] to add. */ fun addIndexingListener(listener: IndexingListener) @@ -72,6 +75,7 @@ interface MusicRepository { /** * Remove an [IndexingListener] such that it does not receive any further updates from this * instance. + * * @param listener The [IndexingListener] to remove. */ fun removeIndexingListener(listener: IndexingListener) @@ -79,13 +83,15 @@ interface MusicRepository { /** * Register an [IndexingWorker] to handle loading operations. Will do nothing if one is already * registered. + * * @param worker The [IndexingWorker] to register. */ fun registerWorker(worker: IndexingWorker) /** - * Unregister an [IndexingWorker] and drop any work currently being done by it. Does nothing - * if given [IndexingWorker] is not the currently registered instance. + * Unregister an [IndexingWorker] and drop any work currently being done by it. Does nothing if + * given [IndexingWorker] is not the currently registered instance. + * * @param worker The [IndexingWorker] to unregister. */ fun unregisterWorker(worker: IndexingWorker) @@ -93,62 +99,56 @@ interface MusicRepository { /** * Request that a music loading operation is started by the current [IndexingWorker]. Does * nothing if one is not available. + * * @param withCache Whether to load with the music cache or not. */ fun requestIndex(withCache: Boolean) /** * Load the music library. Any prior loads will be canceled. + * * @param worker The [IndexingWorker] to perform the work with. * @param withCache Whether to load with the music cache or not. * @return The top-level music loading [Job] started. */ 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 { /** * Called when a change to the stored music information occurs. + * * @param changes The [Changes] that have occured. */ fun onMusicChanges(changes: Changes) } + /** * Flags indicating which kinds of music information changed. + * * @param library Whether the current [Library] has changed. * @param playlists Whether the current [Playlist]s have changed. */ 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 { - /** - * Called when the music loading state changed. - */ + /** Called when the music loading state changed. */ fun onIndexingStateChanged() } - /** - * A persistent worker that can load music in the background. - */ + /** A persistent worker that can load music in the background. */ interface IndexingWorker { - /** - * A [Context] required to read device storage - */ + /** A [Context] required to read device storage */ val context: Context - /** - * The [CoroutineScope] to perform coroutine music loading work on. - */ + /** The [CoroutineScope] to perform coroutine music loading work on. */ val scope: CoroutineScope /** - * Request that the music loading process ([index]) should be started. Any prior - * loads should be canceled. + * Request that the music loading process ([index]) should be started. Any prior loads + * should be canceled. + * * @param withCache Whether to use the music cache when loading. */ fun requestIndex(withCache: Boolean) @@ -301,6 +301,7 @@ constructor( cacheRepository.writeCache(rawSongs) } val newLibrary = libraryJob.await() + // TODO: Make real playlist reading withContext(Dispatchers.Main) { emitComplete(null) emitData(newLibrary, listOf()) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index c5f532e39..6d6af6d46 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -54,6 +54,8 @@ interface MusicSettings : Settings { var artistSort: Sort /** The [Sort] mode used in [Genre] lists. */ var genreSort: Sort + /** The [Sort] mode used in [Playlist] lists. */ + var playlistSort: Sort /** The [Sort] mode used in an [Album]'s [Song] list. */ var albumSongSort: Sort /** 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 get() { var sort = diff --git a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt index dcb80a237..848685ac6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/playlist/PlaylistImpl.kt @@ -30,4 +30,7 @@ class PlaylistImpl(rawPlaylist: RawPlaylist, library: Library, musicSettings: Mu override val rawSortName = null override val sortName = SortName(rawName, musicSettings) override val songs = rawPlaylist.songs.mapNotNull { library.find(it.songUid) } + override val durationMs = songs.sumOf { it.durationMs } + override val albums = + songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 0bf724abf..23c7af7b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -178,6 +178,7 @@ constructor( MusicMode.ALBUMS -> playImpl(song, song.album) MusicMode.ARTISTS -> playFromArtist(song) MusicMode.GENRES -> playFromGenre(song) + MusicMode.PLAYLISTS -> error("Playing from a playlist is not supported.") } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index ce118eee4..c0facb71e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -148,9 +148,9 @@ class SearchFragment : ListFragment() { when (item) { is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item) is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item) - is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item) - is Genre -> openMusicMenu(anchor, R.menu.menu_artist_actions, item) - is Playlist -> TODO("handle this") + is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item) + is Genre -> openMusicMenu(anchor, R.menu.menu_parent_actions, item) + is Playlist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 295eab901..99f857793 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -154,6 +154,7 @@ constructor( MusicMode.ALBUMS -> R.id.option_filter_albums MusicMode.ARTISTS -> R.id.option_filter_artists MusicMode.GENRES -> R.id.option_filter_genres + MusicMode.PLAYLISTS -> R.id.option_filter_all // TODO: Handle // Null maps to filtering nothing. null -> R.id.option_filter_all } diff --git a/app/src/main/res/drawable/ic_playlist_24.xml b/app/src/main/res/drawable/ic_playlist_24.xml new file mode 100644 index 000000000..d92e150b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/menu/menu_artist_actions.xml b/app/src/main/res/menu/menu_parent_actions.xml similarity index 100% rename from app/src/main/res/menu/menu_artist_actions.xml rename to app/src/main/res/menu/menu_parent_actions.xml diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 3b43e79b2..4ca96d8e0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -5,6 +5,7 @@ + 200 100 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 03936b846..5c745d67d 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,7 +9,7 @@ %d %1$s (%2$s) %s - %s - %1$s/%2$s + %1$s/%2$s Vorbis diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 5971baebb..faab5d5cd 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -35,7 +35,7 @@ auxio_wipe_state auxio_restore_state - auxio_lib_tabs + auxio_home_tabs auxio_hide_collaborators auxio_round_covers auxio_bar_action @@ -47,6 +47,7 @@ auxio_albums_sort auxio_artists_sort auxio_genres_sort + auxio_playlists_sort auxio_album_sort auxio_artist_sort diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41d015a28..af89461bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,9 @@ Genre Genres + Playlist + Playlists + Search @@ -307,6 +310,7 @@ Album cover for %s Artist image for %s Genre image for %s + Playlist image for %s