diff --git a/app/src/main/java/org/akanework/gramophone/ui/adapters/TrackInfoAdapter.kt b/app/src/main/java/org/akanework/gramophone/ui/adapters/TrackInfoAdapter.kt new file mode 100644 index 000000000..5cfb2080b --- /dev/null +++ b/app/src/main/java/org/akanework/gramophone/ui/adapters/TrackInfoAdapter.kt @@ -0,0 +1,107 @@ +package org.akanework.gramophone.ui.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.akanework.gramophone.R + +class TrackInfoAdapter(private val context: Context) : + RecyclerView.Adapter() { + private var mediaItems = listOf() + private var shuffleIndices: List? = null + private var currentPlayingPosition = 0 + + fun updatePlaylist(controller: MediaController?) { + val newItems = if (controller != null && controller.mediaItemCount > 0) { + val timeline = controller.currentTimeline + val shuffleEnabled = controller.shuffleModeEnabled + val items = mutableListOf() + val indices = if (shuffleEnabled) mutableListOf() else null + + var index = timeline.getFirstWindowIndex(shuffleEnabled) + var position = 0 + + while (index != C.INDEX_UNSET) { + items.add(controller.getMediaItemAt(index)) + indices?.add(index) + if (index == controller.currentMediaItemIndex) { + currentPlayingPosition = position + } + index = timeline.getNextWindowIndex( + index, + Player.REPEAT_MODE_OFF, + shuffleEnabled) + position++ + } + + shuffleIndices = indices + items + } else { + currentPlayingPosition = 0 + shuffleIndices = null + emptyList() + } + + val diffCallback = TrackDiffCallback(mediaItems, newItems) + val diffResult = DiffUtil.calculateDiff(diffCallback) + + mediaItems = newItems + diffResult.dispatchUpdatesTo(this) + } + + fun getCurrentPosition(): Int = currentPlayingPosition + + fun getCurrentIndex(pos: Int) = shuffleIndices?.getOrNull(pos) ?: pos + + override fun getItemCount() = mediaItems.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_track_info, parent, false) + return TrackViewHolder(view) + } + + override fun onBindViewHolder(holder: TrackViewHolder, position: Int) { + holder.bind(mediaItems[position]) + } + + inner class TrackViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val titleView: TextView = view.findViewById(R.id.track_title) + private val artistView: TextView = view.findViewById(R.id.track_artist) + + fun bind(mediaItem: MediaItem) { + titleView.text = mediaItem.mediaMetadata.title + artistView.text = mediaItem.mediaMetadata.artist + ?: context.getString(R.string.unknown_artist) + } + } + + private class TrackDiffCallback( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].mediaId == newList[newItemPosition].mediaId + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem.mediaMetadata.title == newItem.mediaMetadata.title && + oldItem.mediaMetadata.artist == newItem.mediaMetadata.artist + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/akanework/gramophone/ui/components/PlayerBottomSheet.kt b/app/src/main/java/org/akanework/gramophone/ui/components/PlayerBottomSheet.kt index aa475a21b..db23ef65d 100644 --- a/app/src/main/java/org/akanework/gramophone/ui/components/PlayerBottomSheet.kt +++ b/app/src/main/java/org/akanework/gramophone/ui/components/PlayerBottomSheet.kt @@ -158,11 +158,13 @@ class PlayerBottomSheet private constructor( previewPlayer.alpha = 1f fullPlayer.alpha = 0f bottomSheetBackCallback!!.isEnabled = false + (previewPlayer as? PreviewBottomSheet)?.setSwipeEnabled(true) } BottomSheetBehavior.STATE_DRAGGING, BottomSheetBehavior.STATE_SETTLING -> { fullPlayer.visibility = VISIBLE previewPlayer.visibility = VISIBLE + (previewPlayer as? PreviewBottomSheet)?.setSwipeEnabled(false) } BottomSheetBehavior.STATE_EXPANDED, BottomSheetBehavior.STATE_HALF_EXPANDED -> { @@ -171,6 +173,7 @@ class PlayerBottomSheet private constructor( previewPlayer.alpha = 0f fullPlayer.alpha = 1f bottomSheetBackCallback!!.isEnabled = true + (previewPlayer as? PreviewBottomSheet)?.setSwipeEnabled(false) } BottomSheetBehavior.STATE_HIDDEN -> { diff --git a/app/src/main/java/org/akanework/gramophone/ui/components/PreviewBottomSheet.kt b/app/src/main/java/org/akanework/gramophone/ui/components/PreviewBottomSheet.kt index 01fcf4949..ecff90876 100644 --- a/app/src/main/java/org/akanework/gramophone/ui/components/PreviewBottomSheet.kt +++ b/app/src/main/java/org/akanework/gramophone/ui/components/PreviewBottomSheet.kt @@ -3,14 +3,15 @@ package org.akanework.gramophone.ui.components import android.content.Context import android.util.AttributeSet import android.widget.ImageView -import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.ViewCompat import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Timeline import androidx.media3.session.MediaController +import androidx.viewpager2.widget.ViewPager2 import coil3.asDrawable import coil3.imageLoader import coil3.request.Disposable @@ -23,18 +24,22 @@ import org.akanework.gramophone.R import org.akanework.gramophone.logic.playOrPause import org.akanework.gramophone.logic.startAnimation import org.akanework.gramophone.ui.MainActivity +import org.akanework.gramophone.ui.adapters.TrackInfoAdapter class PreviewBottomSheet(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes), Player.Listener { + private val activity get() = context as MainActivity private val instance: MediaController? get() = activity.getPlayer() + private val bottomSheetPreviewCover: ImageView - private val bottomSheetPreviewTitle: TextView - private val bottomSheetPreviewSubtitle: TextView private val bottomSheetPreviewControllerButton: MaterialButton private val bottomSheetPreviewNextButton: MaterialButton + private val trackInfoPager: ViewPager2 + private val trackAdapter: TrackInfoAdapter + private var lastDisposable: Disposable? = null constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : @@ -44,11 +49,31 @@ class PreviewBottomSheet(context: Context, attrs: AttributeSet?, defStyleAttr: I init { inflate(context, R.layout.preview_player, this) - bottomSheetPreviewTitle = findViewById(R.id.preview_song_name) - bottomSheetPreviewSubtitle = findViewById(R.id.preview_artist_name) + bottomSheetPreviewCover = findViewById(R.id.preview_album_cover) bottomSheetPreviewControllerButton = findViewById(R.id.preview_control) bottomSheetPreviewNextButton = findViewById(R.id.preview_next) + trackInfoPager = findViewById(R.id.track_info_pager) + + trackAdapter = TrackInfoAdapter(context) + trackInfoPager.adapter = trackAdapter + trackInfoPager.offscreenPageLimit = 1 + + trackInfoPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + instance?.let { controller -> + if (position != trackAdapter.getCurrentPosition() && + position < trackAdapter.itemCount) { + ViewCompat.performHapticFeedback( + trackInfoPager, + HapticFeedbackConstantsCompat.CONFIRM + ) + controller.seekTo(trackAdapter.getCurrentIndex(position), 0) + } + } + } + }) bottomSheetPreviewControllerButton.setOnClickListener { ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) @@ -93,7 +118,8 @@ class PreviewBottomSheet(context: Context, attrs: AttributeSet?, defStyleAttr: I mediaItem: MediaItem?, reason: @Player.MediaItemTransitionReason Int ) { - if ((instance?.mediaItemCount ?: 0) > 0) { + val controller = instance + if (controller != null && controller.mediaItemCount > 0) { lastDisposable?.dispose() lastDisposable = context.imageLoader.enqueue(ImageRequest.Builder(context).apply { target(onSuccess = { @@ -106,12 +132,43 @@ class PreviewBottomSheet(context: Context, attrs: AttributeSet?, defStyleAttr: I allowHardware(bottomSheetPreviewCover.isHardwareAccelerated) error(R.drawable.ic_default_cover) }.build()) - bottomSheetPreviewTitle.text = mediaItem?.mediaMetadata?.title - bottomSheetPreviewSubtitle.text = - mediaItem?.mediaMetadata?.artist ?: context.getString(R.string.unknown_artist) + + if (trackAdapter.itemCount == 0) { + trackAdapter.updatePlaylist(controller) + trackInfoPager.setCurrentItem(trackAdapter.getCurrentPosition(), false) + } else { + post { + trackAdapter.updatePlaylist(controller) + if (trackInfoPager.currentItem != trackAdapter.getCurrentPosition()) { + trackInfoPager.setCurrentItem( + trackAdapter.getCurrentPosition(), + true + ) + } + } + } } else { lastDisposable?.dispose() lastDisposable = null + post { + trackAdapter.updatePlaylist(null) + } + } + } + + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + post { + trackAdapter.updatePlaylist(instance) + instance?.let { controller -> + if (controller.mediaItemCount > 0 && + trackInfoPager.currentItem != trackAdapter.getCurrentPosition()) { + trackInfoPager.setCurrentItem(trackAdapter.getCurrentPosition(), false) + } + } } } + + fun setSwipeEnabled(enabled: Boolean) { + trackInfoPager.isUserInputEnabled = enabled + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_track_info.xml b/app/src/main/res/layout/item_track_info.xml new file mode 100644 index 000000000..a4d39cc7b --- /dev/null +++ b/app/src/main/res/layout/item_track_info.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preview_player.xml b/app/src/main/res/layout/preview_player.xml index 2deeee06b..be890641f 100644 --- a/app/src/main/res/layout/preview_player.xml +++ b/app/src/main/res/layout/preview_player.xml @@ -32,32 +32,15 @@ - - - - - - - + app:layout_constraintTop_toTopOf="parent" />