From 7e5cd2acd784ec5ab1efa865fe7c86be7b9a1a97 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 8 Jul 2023 12:11:51 +0300 Subject: [PATCH 1/6] Cover art carousel on playback fragment --- .../auxio/playback/PlaybackPanelFragment.kt | 52 +++++++++++++- .../playback/carousel/CoverCarouselAdapter.kt | 72 +++++++++++++++++++ .../layout-h480dp/fragment_playback_panel.xml | 7 +- .../fragment_playback_panel.xml | 7 +- .../res/layout/fragment_playback_panel.xml | 14 ++-- app/src/main/res/layout/item_cover.xml | 10 +++ 6 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt create mode 100644 app/src/main/res/layout/item_cover.xml 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 af1efd658..74834baf0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -29,7 +29,11 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels +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 @@ -38,6 +42,8 @@ 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.queue.QueueViewModel import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment @@ -64,7 +70,9 @@ class PlaybackPanelFragment : 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 override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -94,6 +102,13 @@ class PlaybackPanelFragment : setOnMenuItemClickListener(this@PlaybackPanelFragment) } + // cover carousel adapter + coverAdapter = CoverCarouselAdapter() + binding.playbackCoverPager.apply { + adapter = coverAdapter + registerOnPageChangeCallback(OnCoverChangedCallback(queueModel)) + } + // Set up marquee on song information, alongside click handlers that navigate to each // respective item. binding.playbackSong.apply { @@ -131,11 +146,14 @@ class PlaybackPanelFragment : collectImmediately(playbackModel.repeatMode, ::updateRepeat) collectImmediately(playbackModel.isPlaying, ::updatePlaying) collectImmediately(playbackModel.isShuffled, ::updateShuffled) + collectImmediately(queueModel.queue, ::updateQueue) + collectImmediately(queueModel.index, ::updateQueuePosition) collect(detailModel.toShow.flow, ::handleShow) } override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { 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 @@ -194,6 +212,18 @@ class PlaybackPanelFragment : playbackModel.seekTo(positionDs) } + private fun updateQueue(queue: List) { + coverAdapter?.update(queue, queueModel.queueInstructions.flow.value) + } + + private fun updateQueuePosition(position: Int) { + val pager = requireBinding().playbackCoverPager + val distance = abs(pager.currentItem - position) + if (distance != 0) { + pager.setCurrentItem(position, distance == 1) + } + } + private fun updateSong(song: Song?) { if (song == null) { // Nothing to do. @@ -203,7 +233,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() logD("Updating song display: $song") - binding.playbackCover.bind(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) @@ -257,4 +287,24 @@ class PlaybackPanelFragment : private fun navigateToCurrentAlbum() { playbackModel.song.value?.let { detailModel.showAlbum(it.album) } } + + private class OnCoverChangedCallback(private val viewModel: QueueViewModel) : + OnPageChangeCallback() { + + private var targetPosition = RecyclerView.NO_POSITION + + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + targetPosition = position + } + + override fun onPageScrollStateChanged(state: Int) { + super.onPageScrollStateChanged(state) + if (state == ViewPager2.SCROLL_STATE_IDLE && + targetPosition != RecyclerView.NO_POSITION && + targetPosition != viewModel.index.value) { + viewModel.goto(targetPosition) + } + } + } } 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 new file mode 100644 index 000000000..0102d5b50 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt @@ -0,0 +1,72 @@ +/* + * 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/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 263bf2a7d..234c0581e 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -16,12 +16,9 @@ app:title="@string/lbl_playback" tools:subtitle="@string/lbl_all_songs" /> - - - - + From 4fa43d59ec3b744a67332525209dc01a6687c7f4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 11 Jul 2023 10:52:52 -0600 Subject: [PATCH 2/6] playback: fix vertical scrolling interception Fix an issue where the cover ViewPager would intercept vertical swipes to collapse the queue sheet. This required manually configuring nested scrolling states in the the internal RecyclerView. --- .../org/oxycblt/auxio/playback/PlaybackPanelFragment.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 74834baf0..e6d3a91c6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -107,6 +107,8 @@ class PlaybackPanelFragment : binding.playbackCoverPager.apply { adapter = coverAdapter registerOnPageChangeCallback(OnCoverChangedCallback(queueModel)) + val recycler = VP_RECYCLER_FIELD.get(this@apply) as RecyclerView + recycler.isNestedScrollingEnabled = false } // Set up marquee on song information, alongside click handlers that navigate to each @@ -307,4 +309,8 @@ class PlaybackPanelFragment : } } } + + private companion object { + val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") + } } From 59ee40b22888fe29f0cfb98ce133b1b8386c2296 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 11 Jul 2023 11:04:37 -0600 Subject: [PATCH 3/6] playback: add missing imports Forgotten from prior backport commit. --- .../java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt | 2 ++ 1 file changed, 2 insertions(+) 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 e6d3a91c6..942e67bd8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -49,10 +49,12 @@ import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat +import java.lang.reflect.Field /** * A [ViewBindingFragment] more information about the currently playing song, alongside all From 69de9d6f2f81cdc1c554c3eacda0690d1a22c342 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 12 Jul 2023 18:08:24 +0300 Subject: [PATCH 4/6] Full song info pager on playback screen --- .../auxio/playback/PlaybackPanelFragment.kt | 63 +++----- .../playback/carousel/CoverCarouselAdapter.kt | 72 --------- .../playback/pager/PlaybackPageListener.kt | 28 ++++ .../playback/pager/PlaybackPagerAdapter.kt | 140 ++++++++++++++++++ .../layout-h480dp/fragment_playback_panel.xml | 43 +----- .../res/layout-h480dp/item_playback_song.xml | 56 +++++++ .../fragment_playback_panel.xml | 41 +---- .../res/layout-sw600dp/item_playback_song.xml | 56 +++++++ .../res/layout/fragment_playback_panel.xml | 44 +----- app/src/main/res/layout/item_cover.xml | 10 -- .../main/res/layout/item_playback_song.xml | 55 +++++++ 11 files changed, 366 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt create mode 100644 app/src/main/res/layout-h480dp/item_playback_song.xml create mode 100644 app/src/main/res/layout-sw600dp/item_playback_song.xml delete mode 100644 app/src/main/res/layout/item_cover.xml create mode 100644 app/src/main/res/layout/item_playback_song.xml 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 From 22db781c0bc9d77398d76a4cb6baff76a4259c0a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 14 Jul 2023 07:54:01 +0300 Subject: [PATCH 5/6] Do not start playing after switching tracks by swype --- .../org/oxycblt/auxio/list/menu/MenuViewModel.kt | 1 + .../auxio/playback/PlaybackPanelFragment.kt | 14 ++++++-------- .../auxio/playback/pager/PlaybackPageListener.kt | 2 +- .../auxio/playback/pager/PlaybackPagerAdapter.kt | 2 +- .../oxycblt/auxio/playback/queue/QueueFragment.kt | 2 +- .../oxycblt/auxio/playback/queue/QueueViewModel.kt | 5 +++-- .../auxio/playback/state/PlaybackStateManager.kt | 7 ++++--- .../auxio/playback/system/MediaSessionComponent.kt | 2 +- 8 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt index 44af342bd..3e34c1a52 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.util.logW /** * Manages the state information for [MenuDialogFragment] implementations. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel 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 67a343723..89b79ae2d 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,6 +33,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import dagger.hilt.android.AndroidEntryPoint +import java.lang.reflect.Field +import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.detail.DetailViewModel @@ -53,8 +55,6 @@ import org.oxycblt.auxio.util.logD 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 @@ -252,8 +252,7 @@ class PlaybackPanelFragment : is Show.AlbumArtistDecision, is Show.GenreDetails, is Show.PlaylistDetails, - null -> { - } + null -> {} } } @@ -286,9 +285,8 @@ class PlaybackPanelFragment : super.onPageScrollStateChanged(state) if (state == ViewPager2.SCROLL_STATE_IDLE && targetPosition != RecyclerView.NO_POSITION && - targetPosition != viewModel.index.value - ) { - viewModel.goto(targetPosition) + targetPosition != viewModel.index.value) { + viewModel.goto(targetPosition, playIfPaused = false) } } } 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 index 6cf93aa22..3a9c56fe1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.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.pager interface PlaybackPageListener { 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 index 1445c3a74..049098794 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt @@ -24,13 +24,13 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import kotlin.jvm.internal.Intrinsics 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, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 2db007971..ca3a924c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -88,7 +88,7 @@ class QueueFragment : ViewBindingFragment(), EditClickList } override fun onClick(item: Song, viewHolder: RecyclerView.ViewHolder) { - queueModel.goto(viewHolder.bindingAdapterPosition) + queueModel.goto(viewHolder.bindingAdapterPosition, playIfPaused = true) } override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 5b1edce73..12a51bbf7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -106,13 +106,14 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt * * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of * range. + * @param playIfPaused Start playing after switching even if it currently is paused */ - fun goto(adapterIndex: Int) { + fun goto(adapterIndex: Int, playIfPaused: Boolean) { if (adapterIndex !in queue.value.indices) { return } logD("Going to position $adapterIndex in queue") - playbackManager.goto(adapterIndex) + playbackManager.goto(adapterIndex, playIfPaused || playbackManager.playerState.isPlaying) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 388087653..ecbce22ee 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -120,8 +120,9 @@ interface PlaybackStateManager { * Play a [Song] at the given position in the queue. * * @param index The position of the [Song] in the queue to start playing. + * @param play Whether to start playing after switching to target index */ - fun goto(index: Int) + fun goto(index: Int, play: Boolean) /** * Add [Song]s to the top of the queue. @@ -426,12 +427,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } @Synchronized - override fun goto(index: Int) { + override fun goto(index: Int, play: Boolean) { val internalPlayer = internalPlayer ?: return if (queue.goto(index)) { logD("Moving to $index") notifyIndexMoved() - internalPlayer.loadSong(queue.currentSong, true) + internalPlayer.loadSong(queue.currentSong, play) } else { logW("$index was not in bounds, could not move to it") } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 369675f17..b6e0819d0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -256,7 +256,7 @@ constructor( } override fun onSkipToQueueItem(id: Long) { - playbackManager.goto(id.toInt()) + playbackManager.goto(id.toInt(), true) } override fun onCustomAction(action: String?, extras: Bundle?) { From a34128154832af1564a2896213b9c058e3493f74 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 14 Jul 2023 08:02:30 +0300 Subject: [PATCH 6/6] Open menu by tap on cover --- .../java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt | 4 ++++ .../org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt | 2 ++ .../org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) 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 89b79ae2d..55f17ec3f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -271,6 +271,10 @@ class PlaybackPanelFragment : playbackModel.song.value?.let { detailModel.showAlbum(it.album) } } + override fun navigateToMenu() { + binding?.playbackToolbar?.showOverflowMenu() + } + private class OnCoverChangedCallback(private val viewModel: QueueViewModel) : OnPageChangeCallback() { 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 index 3a9c56fe1..a47c88494 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPageListener.kt @@ -25,4 +25,6 @@ interface PlaybackPageListener { fun navigateToCurrentAlbum() fun navigateToCurrentSong() + + fun navigateToMenu() } 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 index 049098794..f171677cc 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/pager/PlaybackPagerAdapter.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.pager import android.view.View @@ -63,6 +63,7 @@ private constructor( binding.playbackSong.setOnClickListener(this) binding.playbackArtist.setOnClickListener(this) binding.playbackAlbum.setOnClickListener(this) + binding.playbackCover.setOnClickListener(this) } override fun onClick(v: View) { @@ -70,6 +71,7 @@ private constructor( R.id.playback_album -> listener.navigateToCurrentAlbum() R.id.playback_artist -> listener.navigateToCurrentArtist() R.id.playback_song -> listener.navigateToCurrentSong() + R.id.playback_cover -> listener.navigateToMenu() } }