diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 942e67bd8..67a343723 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.playback import android.content.ActivityNotFoundException @@ -33,7 +33,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import dagger.hilt.android.AndroidEntryPoint -import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.detail.DetailViewModel @@ -41,8 +40,8 @@ import org.oxycblt.auxio.detail.Show import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.playback.carousel.CoverCarouselAdapter +import org.oxycblt.auxio.playback.pager.PlaybackPageListener +import org.oxycblt.auxio.playback.pager.PlaybackPagerAdapter import org.oxycblt.auxio.playback.queue.QueueViewModel import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar @@ -55,6 +54,7 @@ import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat import java.lang.reflect.Field +import kotlin.math.abs /** * A [ViewBindingFragment] more information about the currently playing song, alongside all @@ -68,13 +68,14 @@ import java.lang.reflect.Field class PlaybackPanelFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener, - StyledSeekBar.Listener { + StyledSeekBar.Listener, + PlaybackPageListener { private val playbackModel: PlaybackViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() private val queueModel: QueueViewModel by activityViewModels() private var equalizerLauncher: ActivityResultLauncher? = null - private var coverAdapter: CoverCarouselAdapter? = null + private var coverAdapter: PlaybackPagerAdapter? = null override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -105,7 +106,7 @@ class PlaybackPanelFragment : } // cover carousel adapter - coverAdapter = CoverCarouselAdapter() + coverAdapter = PlaybackPagerAdapter(this, viewLifecycleOwner) binding.playbackCoverPager.apply { adapter = coverAdapter registerOnPageChangeCallback(OnCoverChangedCallback(queueModel)) @@ -113,26 +114,6 @@ class PlaybackPanelFragment : recycler.isNestedScrollingEnabled = false } - // Set up marquee on song information, alongside click handlers that navigate to each - // respective item. - binding.playbackSong.apply { - isSelected = true - setOnClickListener { - playbackModel.song.value?.let { - detailModel.showAlbum(it) - playbackModel.openMain() - } - } - } - binding.playbackArtist.apply { - isSelected = true - setOnClickListener { navigateToCurrentArtist() } - } - binding.playbackAlbum.apply { - isSelected = true - setOnClickListener { navigateToCurrentAlbum() } - } - binding.playbackSeekBar.listener = this // Set up actions @@ -159,10 +140,6 @@ class PlaybackPanelFragment : equalizerLauncher = null coverAdapter = null binding.playbackToolbar.setOnMenuItemClickListener(null) - // Marquee elements leak if they are not disabled when the views are destroyed. - binding.playbackSong.isSelected = false - binding.playbackArtist.isSelected = false - binding.playbackAlbum.isSelected = false } override fun onMenuItemClick(item: MenuItem) = @@ -235,12 +212,7 @@ class PlaybackPanelFragment : } val binding = requireBinding() - val context = requireContext() logD("Updating song display: $song") - // binding.playbackCover.bind(song) - binding.playbackSong.text = song.name.resolve(context) - binding.playbackArtist.text = song.artists.resolveNames(context) - binding.playbackAlbum.text = song.album.name.resolve(context) binding.playbackSeekBar.durationDs = song.durationMs.msToDs() } @@ -280,15 +252,23 @@ class PlaybackPanelFragment : is Show.AlbumArtistDecision, is Show.GenreDetails, is Show.PlaylistDetails, - null -> {} + null -> { + } } } - private fun navigateToCurrentArtist() { + override fun navigateToCurrentSong() { + playbackModel.song.value?.let { + detailModel.showAlbum(it) + playbackModel.openMain() + } + } + + override fun navigateToCurrentArtist() { playbackModel.song.value?.let(detailModel::showArtist) } - private fun navigateToCurrentAlbum() { + override fun navigateToCurrentAlbum() { playbackModel.song.value?.let { detailModel.showAlbum(it.album) } } @@ -306,12 +286,13 @@ class PlaybackPanelFragment : super.onPageScrollStateChanged(state) if (state == ViewPager2.SCROLL_STATE_IDLE && targetPosition != RecyclerView.NO_POSITION && - targetPosition != viewModel.index.value) { + targetPosition != viewModel.index.value + ) { viewModel.goto(targetPosition) } } } - + private companion object { val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt deleted file mode 100644 index 0102d5b50..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * CoverCarouselAdapter.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.playback.carousel - -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.databinding.ItemCoverBinding -import org.oxycblt.auxio.list.adapter.FlexibleListAdapter -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.inflater - -class CoverCarouselAdapter : - FlexibleListAdapter(CoverViewHolder.DIFF_CALLBACK) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoverViewHolder { - return CoverViewHolder.from(parent) - } - - override fun onBindViewHolder(holder: CoverViewHolder, position: Int) { - holder.bind(getItem(position)) - } -} - -class CoverViewHolder private constructor(private val binding: ItemCoverBinding) : - RecyclerView.ViewHolder(binding.root) { - /** - * Bind new data to this instance. - * - * @param item The new [Song] to bind. - */ - fun bind(item: Song) { - binding.playbackCover.bind(item) - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: ViewGroup) = - CoverViewHolder(ItemCoverBinding.inflate(parent.context.inflater, parent, false)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Song, newItem: Song) = - oldItem.uid == newItem.uid - - override fun areContentsTheSame(oldItem: Song, newItem: Song) = - oldItem.album.coverUri == newItem.album.coverUri - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt new file mode 100644 index 000000000..6cf93aa22 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Auxio Project + * PlaybackPageListener.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.playback.pager + +interface PlaybackPageListener { + + fun navigateToCurrentArtist() + + fun navigateToCurrentAlbum() + + fun navigateToCurrentSong() +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt new file mode 100644 index 000000000..1445c3a74 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 Auxio Project + * PlaybackPagerAdapter.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.playback.pager + +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemPlaybackSongBinding +import org.oxycblt.auxio.list.adapter.FlexibleListAdapter +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.util.inflater +import kotlin.jvm.internal.Intrinsics + +class PlaybackPagerAdapter( + private val listener: PlaybackPageListener, + private val lifecycleOwner: LifecycleOwner +) : FlexibleListAdapter(CoverViewHolder.DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoverViewHolder { + return CoverViewHolder.from(parent, listener).also { + lifecycleOwner.lifecycle.addObserver(it) + } + } + + override fun onBindViewHolder(holder: CoverViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun onViewRecycled(holder: CoverViewHolder) { + holder.recycle() + super.onViewRecycled(holder) + } +} + +class CoverViewHolder +private constructor( + private val binding: ItemPlaybackSongBinding, + private val listener: PlaybackPageListener +) : RecyclerView.ViewHolder(binding.root), DefaultLifecycleObserver, View.OnClickListener { + + init { + binding.playbackSong.setOnClickListener(this) + binding.playbackArtist.setOnClickListener(this) + binding.playbackAlbum.setOnClickListener(this) + } + + override fun onClick(v: View) { + when (v.id) { + R.id.playback_album -> listener.navigateToCurrentAlbum() + R.id.playback_artist -> listener.navigateToCurrentArtist() + R.id.playback_song -> listener.navigateToCurrentSong() + } + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + setSelected(true) + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + setSelected(false) + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + owner.lifecycle.removeObserver(this) + } + + /** + * Bind new data to this instance. + * + * @param item The new [Song] to bind. + */ + fun bind(item: Song) { + binding.playbackCover.bind(item) + val context = binding.root.context + binding.playbackSong.text = item.name.resolve(context) + binding.playbackArtist.text = item.artists.resolveNames(context) + binding.playbackAlbum.text = item.album.name.resolve(context) + setSelected(true) + } + + fun recycle() { + // Marquee elements leak if they are not disabled when the views are destroyed. + setSelected(false) + } + + private fun setSelected(value: Boolean) { + binding.playbackSong.isSelected = value + binding.playbackArtist.isSelected = value + binding.playbackAlbum.isSelected = value + } + + companion object { + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: ViewGroup, listener: PlaybackPageListener) = + CoverViewHolder( + ItemPlaybackSongBinding.inflate(parent.context.inflater, parent, false), + listener + ) + + /** A comparator that can be used with DiffUtil. */ + val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Song, newItem: Song) = + oldItem.uid == newItem.uid + + override fun areContentsTheSame(oldItem: Song, newItem: Song): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + } + } +} diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 234c0581e..28f69b2b2 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -18,49 +18,12 @@ - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml index c0b2af850..adeac9d27 100644 --- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml @@ -18,49 +18,12 @@ - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 23ce8412e..e873722e1 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -18,56 +18,20 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_cover.xml b/app/src/main/res/layout/item_cover.xml deleted file mode 100644 index 223738538..000000000 --- a/app/src/main/res/layout/item_cover.xml +++ /dev/null @@ -1,10 +0,0 @@ - - diff --git a/app/src/main/res/layout/item_playback_song.xml b/app/src/main/res/layout/item_playback_song.xml new file mode 100644 index 000000000..3e8c0c6a1 --- /dev/null +++ b/app/src/main/res/layout/item_playback_song.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file