diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 6753f4ac9..9136e7232 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -16,6 +16,7 @@ import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.recycler.CenterSmoothScroller +import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.ui.createToast import org.oxycblt.auxio.ui.setupAlbumSongActions @@ -87,7 +88,7 @@ class AlbumDetailFragment : DetailFragment() { setupRecycler(detailAdapter) - // -- VIEWMODEL SETUP --- + // -- DETAILVIEWMODEL SETUP --- detailModel.albumSortMode.observe(viewLifecycleOwner) { mode -> logD("Updating sort mode to $mode") @@ -129,6 +130,51 @@ class AlbumDetailFragment : DetailFragment() { } } + // --- PLAYBACKVIEWMODEL SETUP --- + + playbackModel.song.observe(viewLifecycleOwner) { song -> + if (playbackModel.mode.value == PlaybackMode.IN_ALBUM && + playbackModel.parent.value!!.id == detailModel.currentAlbum.value!!.id + ) { + detailAdapter.setCurrentSong(song) + + lastHolder?.setHighlighted(false) + lastHolder = null + + if (song != null) { + // Use existing data instead of having to re-sort it. + val pos = detailAdapter.currentList.indexOfFirst { + it.name == song.name + } + + // Check if the ViewHolder for this song is visible, if it is then highlight it. + // If the ViewHolder is not visible, then the adapter should take care of it if it does become visible. + binding.detailRecycler.layoutManager?.findViewByPosition(pos)?.let { child -> + binding.detailRecycler.getChildViewHolder(child)?.let { + lastHolder = it as Highlightable + + lastHolder?.setHighlighted(true) + } + } + } + } else { + // Clear the viewholders if the mode isn't ALL_SONGS + detailAdapter.setCurrentSong(null) + + lastHolder?.setHighlighted(false) + lastHolder = null + } + } + + playbackModel.isInUserQueue.observe(viewLifecycleOwner) { + if (it) { + // Remove any highlighted ViewHolders if the playback is in the user queue. + detailAdapter.setCurrentSong(null) + lastHolder?.setHighlighted(false) + lastHolder = null + } + } + logD("Fragment created.") return binding.root diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 7ed402728..ede00d17c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.ui.isLandscape import org.oxycblt.auxio.ui.memberBinding @@ -28,6 +29,7 @@ abstract class DetailFragment : Fragment() { protected val binding: FragmentDetailBinding by memberBinding( FragmentDetailBinding::inflate ) + protected var lastHolder: Highlightable? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) @@ -45,6 +47,12 @@ abstract class DetailFragment : Fragment() { callback.isEnabled = false } + override fun onDestroyView() { + super.onDestroyView() + + lastHolder = null + } + /** * Shortcut method for doing setup of the detail toolbar. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt index fc70addde..090a01e7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt @@ -13,8 +13,11 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.recycler.DiffCallback +import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder +import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.disable +import org.oxycblt.auxio.ui.setTextColorResource /** * An adapter for displaying the details and [Song]s of an [Album] @@ -25,6 +28,10 @@ class AlbumDetailAdapter( private val doOnClick: (data: Song) -> Unit, private val doOnLongClick: (data: Song, view: View) -> Unit ) : ListAdapter(DiffCallback()) { + + private var currentSong: Song? = null + private var lastHolder: Highlightable? = null + override fun getItemViewType(position: Int): Int { return when (getItem(position)) { is Album -> ALBUM_HEADER_ITEM_TYPE @@ -51,6 +58,30 @@ class AlbumDetailAdapter( is Album -> (holder as AlbumHeaderViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item) } + + if (currentSong != null && position > 0) { + if (getItem(position).id == currentSong?.id) { + // Reset the last ViewHolder before assigning the new, correct one to be highlighted + lastHolder?.setHighlighted(false) + lastHolder = (holder as Highlightable) + holder.setHighlighted(true) + } else { + (holder as Highlightable).setHighlighted(false) + } + } + } + + /** + * Update the current song that this adapter should be watching for to highlight. + * @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders + */ + fun setCurrentSong(song: Song?) { + // Clear out the last ViewHolder as a song update usually signifies that this current + // ViewHolder is likely invalid. + lastHolder?.setHighlighted(false) + lastHolder = null + + currentSong = song } inner class AlbumHeaderViewHolder( @@ -70,12 +101,25 @@ class AlbumDetailAdapter( inner class AlbumSongViewHolder( private val binding: ItemAlbumSongBinding, - ) : BaseViewHolder(binding, doOnClick, doOnLongClick) { + ) : BaseViewHolder(binding, doOnClick, doOnLongClick), Highlightable { + private val normalTextColor = binding.songName.currentTextColor + private val inactiveTextColor = binding.songTrack.currentTextColor + override fun onBind(data: Song) { binding.song = data binding.songName.requestLayout() } + + override fun setHighlighted(isHighlighted: Boolean) { + if (isHighlighted) { + binding.songName.setTextColorResource(accent.first) + binding.songTrack.setTextColorResource(accent.first) + } else { + binding.songName.setTextColor(normalTextColor) + binding.songTrack.setTextColor(inactiveTextColor) + } + } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index c527e0c1c..ce9e63c4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -78,10 +78,8 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { if (it.itemId != R.id.action_search) { libraryModel.updateSortMode(it.itemId) } else { - // Do whatever this is in order to make the SearchView focusable. - (it.actionView as SearchView).isIconified = false - - // Then also do a basic animation + // Then also do a basic animation on the enter transition. Not done on exit + // because that causes issues with the SearchView. TransitionManager.beginDelayedTransition( binding.libraryToolbar, Fade() ) @@ -98,14 +96,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { searchView.queryHint = getString(R.string.hint_search_library) searchView.maxWidth = Int.MAX_VALUE searchView.setOnQueryTextListener(this@LibraryFragment) - searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> - item.isVisible = !hasFocus - } item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem): Boolean { binding.libraryRecycler.adapter = searchAdapter setGroupVisible(R.id.group_sorting, false) + item.isVisible = false libraryModel.resetQuery() @@ -115,6 +111,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { override fun onMenuItemActionCollapse(item: MenuItem): Boolean { binding.libraryRecycler.adapter = libraryAdapter setGroupVisible(R.id.group_sorting, true) + item.isVisible = true libraryModel.resetQuery() diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index f739677b9..803e6adbf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -47,10 +47,20 @@ data class Song( } } + /** + * Apply a genre to a song. + * @throws IllegalArgumentException When a genre is already applied. + */ fun applyGenre(genre: Genre) { + check(mGenre == null) { "Genre is already applied" } + mGenre = genre } + /** + * Apply an album to a song. + * @throws IllegalArgumentException When a album is already applied. + */ fun applyAlbum(album: Album) { check(mAlbum == null) { "Album is already applied" } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt index 5a44ecdb2..66876e777 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -26,7 +26,9 @@ import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory import com.google.android.exoplayer2.mediacodec.MediaCodecSelector +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -345,7 +347,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca ) } - return SimpleExoPlayer.Builder(this, audioRenderer).build() + val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true) + + return SimpleExoPlayer.Builder(this, audioRenderer) + .setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory)) + .build() } /** diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt index 3b28de8c7..f2d2c4419 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt @@ -1,15 +1,10 @@ package org.oxycblt.auxio.songs -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.recycler.Highlightable -import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder -import org.oxycblt.auxio.ui.accent -import org.oxycblt.auxio.ui.setTextColorResource +import org.oxycblt.auxio.recycler.viewholders.SongViewHolder /** * The adapter for [SongsFragment], shows basic songs without durations. @@ -21,61 +16,15 @@ class SongsAdapter( private val data: List, private val doOnClick: (data: Song) -> Unit, private val doOnLongClick: (data: Song, view: View) -> Unit -) : RecyclerView.Adapter() { - -/* private var currentSong: Song? = null - private var lastHolder: Highlightable? = null*/ +) : RecyclerView.Adapter() { override fun getItemCount(): Int = data.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder { - return SongViewHolder( - ItemSongBinding.inflate(LayoutInflater.from(parent.context)) - ) + return SongViewHolder.from(parent.context, doOnClick, doOnLongClick) } override fun onBindViewHolder(holder: SongViewHolder, position: Int) { holder.bind(data[position]) - -/* if (currentSong != null) { - if (data[position].id == currentSong?.id) { - // Reset the last ViewHolder before assigning the new, correct one to be highlighted - lastHolder?.setHighlighted(false) - lastHolder = holder - holder.setHighlighted(true) - } else { - holder.setHighlighted(false) - } - }*/ - } - -/* fun setCurrentSong(song: Song?) { - // Clear out the last ViewHolder as a song update usually signifies that this current - // ViewHolder is likely invalid. - lastHolder?.setHighlighted(false) - lastHolder = null - - currentSong = song - }*/ - - inner class SongViewHolder( - private val binding: ItemSongBinding - ) : BaseViewHolder(binding, doOnClick, doOnLongClick), Highlightable { - private val normalTextColor = binding.songName.currentTextColor - - override fun onBind(data: Song) { - binding.song = data - - binding.songName.requestLayout() - binding.songInfo.requestLayout() - } - - override fun setHighlighted(isHighlighted: Boolean) { - if (isHighlighted) { - binding.songName.setTextColorResource(accent.first) - } else { - binding.songName.setTextColor(normalTextColor) - } - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 5a6f82b42..318b3946c 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -20,7 +20,6 @@ import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.getLandscapeSpans @@ -70,8 +69,6 @@ class SongsFragment : Fragment() { } ) - var lastHolder: Highlightable? = null - // --- UI SETUP --- binding.songToolbar.apply { @@ -106,48 +103,6 @@ class SongsFragment : Fragment() { // --- VIEWMODEL SETUP --- - /* - Unused, not needed for SongsFragment - TODO: Move this code over to AlbumDetailFragment - playbackModel.song.observe(viewLifecycleOwner) { song -> - if (playbackModel.mode.value == PlaybackMode.ALL_SONGS) { - songAdapter.setCurrentSong(song) - - lastHolder?.setHighlighted(false) - lastHolder = null - - if (song != null) { - val pos = musicStore.songs.indexOfFirst { it.id == song.id } - - // Check if the ViewHolder for this song is visible, if it is then highlight it. - // If it isn't, SongsAdapter will take care of it when it is visible. - binding.songRecycler.layoutManager?.findViewByPosition(pos)?.let { child -> - binding.songRecycler.getChildViewHolder(child)?.let { - lastHolder = it as Highlightable - - lastHolder?.setHighlighted(true) - } - } - } - } else { - // Clear the viewholders if the mode isnt ALL_SONGS - songAdapter.setCurrentSong(null) - - lastHolder?.setHighlighted(false) - lastHolder = null - } - } - - playbackModel.isInUserQueue.observe(viewLifecycleOwner) { - if (it) { - // Remove any highlighted ViewHolders if the playback is in the user queue. - songAdapter.setCurrentSong(null) - lastHolder?.setHighlighted(false) - lastHolder = null - } - } - */ - setupFastScroller(binding) logD("Fragment created.")