diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 4cadda0d..8be6044f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -73,6 +73,7 @@ import org.moire.ultrasonic.util.ShortcutUtil import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.UncaughtExceptionHandler import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber /** @@ -179,11 +180,7 @@ class NavigationActivity : AppCompatActivity() { currentFragmentId = destination.id // Handle the hiding of the NowPlaying fragment when the Player is active - if (currentFragmentId == R.id.playerFragment) { - hideNowPlaying() - } else { - if (!nowPlayingHidden) showNowPlaying() - } + computeNowPlayingVisibility() } // Determine if this is a first run @@ -199,15 +196,16 @@ class NavigationActivity : AppCompatActivity() { Util.ensurePermissionToPostNotification(this) rxBusSubscription += RxBus.dismissNowPlayingCommandObservable.subscribe { - nowPlayingHidden = true - hideNowPlaying() + computeNowPlayingVisibility(false) } rxBusSubscription += RxBus.playerStateObservable.subscribe { - if (it.state == STATE_READY) - showNowPlaying() - else - hideNowPlaying() + // If state is ready then nowPlaying will be visible again + if (it.state == STATE_READY) { + computeNowPlayingVisibility(true) + } else { + computeNowPlayingVisibility() + } } rxBusSubscription += RxBus.themeChangedEventObservable.subscribe { @@ -314,8 +312,7 @@ class NavigationActivity : AppCompatActivity() { // Lifecycle support's constructor registers some event receivers so it should be created early lifecycleSupport.onCreate() - if (!nowPlayingHidden) showNowPlaying() - else hideNowPlaying() + computeNowPlayingVisibility() } /* @@ -555,9 +552,14 @@ class NavigationActivity : AppCompatActivity() { } } - private fun showNowPlaying() { + private fun computeNowPlayingVisibility(forceNewVisibility: Boolean? = null) { + forceNewVisibility.ifNotNull { nowPlayingHidden = !it } if (!Settings.showNowPlaying) { - hideNowPlaying() + nowPlayingView?.visibility = View.GONE + return + } + if (nowPlayingHidden) { + nowPlayingView?.visibility = View.GONE return } @@ -566,7 +568,7 @@ class NavigationActivity : AppCompatActivity() { nowPlayingHidden = false // Do not show for Player fragment if (currentFragmentId == R.id.playerFragment) { - hideNowPlaying() + nowPlayingView?.visibility = View.GONE return } @@ -578,15 +580,11 @@ class NavigationActivity : AppCompatActivity() { nowPlayingView?.visibility = View.VISIBLE } } else { - hideNowPlaying() + nowPlayingView?.visibility = View.GONE } } } - private fun hideNowPlaying() { - nowPlayingView?.visibility = View.GONE - } - private fun setMenuForServerCapabilities() { val isOnline = !ActiveServerProvider.isOffline() val activeServer = activeServerProvider.getActiveServer() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 70eaf028..8966bb71 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -10,18 +10,19 @@ package org.moire.ultrasonic.fragment import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.media3.common.MediaItem +import androidx.media3.common.Player import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.button.MaterialButton import io.reactivex.rxjava3.disposables.Disposable -import java.lang.Exception -import kotlin.math.abs import org.koin.android.ext.android.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R @@ -38,9 +39,88 @@ import timber.log.Timber * Contains the mini-now playing information box displayed at the bottom of the screen */ class NowPlayingFragment : Fragment() { + private var isInitialized = false + private lateinit var nowPlayingCollectionAdapter: NowPlayingCollectionAdapter + private lateinit var viewPager: ViewPager2 + private var rxBusSubscription: Disposable? = null - private var downX = 0f - private var downY = 0f + private val mediaPlayerManager: MediaPlayerManager by inject() + private val pageChangeCallback = PageChangeCallback() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.now_playing_wrapper, container, false) + } + + override fun onResume() { + super.onResume() + if (mediaPlayerManager.currentMediaItemIndex >= 0) + viewPager.setCurrentItem(mediaPlayerManager.currentMediaItemIndex, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + nowPlayingCollectionAdapter = NowPlayingCollectionAdapter(this, mediaPlayerManager) + viewPager = view.findViewById(R.id.pager) + viewPager.currentItem = mediaPlayerManager.currentMediaItemIndex + viewPager.adapter = nowPlayingCollectionAdapter + isInitialized = false + + // Subscribe to updates on current Item + rxBusSubscription = RxBus.playerStateObservable.subscribe { + if (it.state == Player.STATE_READY) { + viewPager.setCurrentItem(it.index, true) + isInitialized = true + } + } + + viewPager.registerOnPageChangeCallback(pageChangeCallback) + } + + override fun onDestroy() { + super.onDestroy() + rxBusSubscription?.dispose() + viewPager.unregisterOnPageChangeCallback(pageChangeCallback) + } + + private inner class PageChangeCallback : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + if (!isInitialized) return + val newIndex = mediaPlayerManager.getUnshuffledIndexOf(position) + if (mediaPlayerManager.currentMediaItemIndex != newIndex) { + mediaPlayerManager.seekTo(newIndex, 0) + } + } + } +} + +class NowPlayingCollectionAdapter( + fragment: Fragment, + private val playerManager: MediaPlayerManager +) : FragmentStateAdapter(fragment) { + + override fun getItemCount(): Int { + return playerManager.mediaItemCount + } + + override fun getItemId(position: Int): Long { + return playerManager.getMediaItemAt(playerManager.getUnshuffledIndexOf(position)) + .hashCode().toLong() + } + + override fun createFragment(position: Int): Fragment { + // Return a NEW fragment instance in createFragment(int) + val mediaItem = playerManager.getMediaItemAt(playerManager.getUnshuffledIndexOf(position)) + return NowPlayingChildFragment(mediaItem) + } +} + +/** + * Contains the mini-now playing information box displayed at the bottom of the screen + */ +class NowPlayingChildFragment(private val mediaItem: MediaItem?) : Fragment() { private var playButton: MaterialButton? = null private var nowPlayingAlbumArtImage: ImageView? = null @@ -79,7 +159,7 @@ class NowPlayingFragment : Fragment() { override fun onDestroy() { super.onDestroy() - rxBusSubscription!!.dispose() + rxBusSubscription?.dispose() } @SuppressLint("ClickableViewAccessibility") @@ -91,7 +171,7 @@ class NowPlayingFragment : Fragment() { playButton!!.setIconResource(R.drawable.media_start) } - val file = mediaPlayerManager.currentMediaItem?.toTrack() + val file = mediaItem?.toTrack() if (file != null) { val title = file.title @@ -121,53 +201,14 @@ class NowPlayingFragment : Fragment() { } } - requireView().setOnTouchListener { _: View?, event: MotionEvent -> - handleOnTouch(event) + requireView().setOnClickListener { + Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) + .navigate(R.id.playerFragment) } - // This empty onClickListener is necessary for the onTouchListener to work - requireView().setOnClickListener { } playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() } } catch (all: Exception) { Timber.w(all, "Failed to get notification cover art") } } - - private fun handleOnTouch(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - downX = event.x - downY = event.y - } - - MotionEvent.ACTION_UP -> { - val upX = event.x - val upY = event.y - val deltaX = downX - upX - val deltaY = downY - upY - - if (abs(deltaX) > MIN_DISTANCE) { - // left or right - if (deltaX < 0) { - mediaPlayerManager.seekToPrevious() - } - if (deltaX > 0) { - mediaPlayerManager.seekToNext() - } - } else if (abs(deltaY) > MIN_DISTANCE) { - if (deltaY < 0) { - RxBus.dismissNowPlayingCommandPublisher.onNext(Unit) - } - } else { - Navigation.findNavController(requireActivity(), R.id.nav_host_fragment) - .navigate(R.id.playerFragment) - } - } - } - return false - } - - companion object { - private const val MIN_DISTANCE = 30 - } } diff --git a/ultrasonic/src/main/res/layout/now_playing_wrapper.xml b/ultrasonic/src/main/res/layout/now_playing_wrapper.xml new file mode 100644 index 00000000..e5937133 --- /dev/null +++ b/ultrasonic/src/main/res/layout/now_playing_wrapper.xml @@ -0,0 +1,14 @@ + + + +