funkwhale-app-android/app/src/main/java/audio/funkwhale/ffa/fragments/NowPlayingFragment.kt

245 lines
8.1 KiB
Kotlin

package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController
import audio.funkwhale.ffa.MainNavDirections
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.FragmentNowPlayingBinding
import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.FFACache
import audio.funkwhale.ffa.utils.ProgressBus
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toIntOrElse
import audio.funkwhale.ffa.utils.untilNetwork
import audio.funkwhale.ffa.viewmodel.NowPlayingViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Float.max
class NowPlayingFragment: Fragment(R.layout.fragment_now_playing) {
private val binding by lazy { FragmentNowPlayingBinding.bind(requireView()) }
private val viewModel by viewModels<NowPlayingViewModel>()
private val favoriteRepository by lazy { FavoritesRepository(requireContext()) }
private val favoritedRepository by lazy { FavoritedRepository(requireContext()) }
private var onDetailsMenuItemClickedCb: () -> Unit = {}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.lifecycleOwner = viewLifecycleOwner
viewModel.currentTrack.distinctUntilChanged().observe(viewLifecycleOwner, ::onTrackChange)
with(binding.controls) {
currentTrackTitle = viewModel.currentTrackTitle
currentTrackArtist = viewModel.currentTrackArtist
isCurrentTrackFavorite = viewModel.isCurrentTrackFavorite
repeatModeResource = viewModel.repeatModeResource
repeatModeAlpha = viewModel.repeatModeAlpha
currentProgressText = viewModel.currentProgressText
currentDurationText = viewModel.currentDurationText
isPlaying = viewModel.isPlaying
progress = viewModel.progress
nowPlayingDetailsPrevious.setOnClickListener {
CommandBus.send(Command.PreviousTrack)
}
nowPlayingDetailsNext.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
nowPlayingDetailsToggle.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
nowPlayingDetailsRepeat.setOnClickListener { toggleRepeatMode() }
nowPlayingDetailsProgress.setOnSeekBarChangeListener(OnSeekBarChanged())
nowPlayingDetailsFavorite.setOnClickListener { onFavorite() }
nowPlayingDetailsAddToPlaylist.setOnClickListener { onAddToPlaylist() }
}
binding.nowPlayingDetailsInfo.setOnClickListener { openInfoMenu() }
with(binding.header) {
lifecycleOwner = viewLifecycleOwner
isBuffering = viewModel.isBuffering
isPlaying = viewModel.isPlaying
progress = viewModel.progress
currentTrackTitle = viewModel.currentTrackTitle
currentTrackArtist = viewModel.currentTrackArtist
nowPlayingNext.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
nowPlayingToggle.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
}
lifecycleScope.launch(Dispatchers.Main) {
CommandBus.get().collect { onCommand(it) }
}
lifecycleScope.launch(Dispatchers.Main) {
ProgressBus.get().collect { onProgress(it) }
}
}
fun onBottomSheetDrag(value: Float) {
binding.nowPlayingRoot.progress = max(value, 0f)
}
fun onDetailsMenuItemClicked(cb: () -> Unit) {
onDetailsMenuItemClickedCb = cb
}
private fun toggleRepeatMode() {
val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0)
val iteratedRepeatMode = (cachedRepeatMode + 1) % 3
FFACache.set(requireContext(), "repeat", "$iteratedRepeatMode")
CommandBus.send(Command.SetRepeatMode(iteratedRepeatMode))
}
private fun onAddToPlaylist() {
val currentTrack = viewModel.currentTrack.value ?: return
CommandBus.send(Command.AddToPlaylist(listOf(currentTrack)))
}
private fun onCommand(command: Command) = when (command) {
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
is Command.SetRepeatMode -> viewModel.repeatMode.postValue(command.mode)
else -> {}
}
private fun onFavorite() {
val currentTrack = viewModel.currentTrack.value ?: return
if (currentTrack.favorite) favoriteRepository.deleteFavorite(currentTrack.id)
else favoriteRepository.addFavorite(currentTrack.id)
currentTrack.favorite = !currentTrack.favorite
// Trigger UI refresh
viewModel.currentTrack.postValue(viewModel.currentTrack.value)
favoritedRepository.fetch(Repository.Origin.Network.origin)
}
private fun onProgress(state: Triple<Int, Int, Int>) {
val (current, duration, percent) = state
val currentMins = (current / 1000) / 60
val currentSecs = (current / 1000) % 60
val durationMins = duration / 60
val durationSecs = duration % 60
viewModel.progress.postValue(percent)
viewModel.currentProgressText.postValue("%02d:%02d".format(currentMins, currentSecs))
viewModel.currentDurationText.postValue("%02d:%02d".format(durationMins, durationSecs))
}
private fun onTrackChange(track: Track?) {
if (track == null) {
binding.header.nowPlayingCover.setImageResource(R.drawable.cover)
return
}
CoverArt.requestCreator(maybeNormalizeUrl(track.album?.cover()))
.into(binding.header.nowPlayingCover)
}
private fun openInfoMenu() {
val currentTrack = viewModel.currentTrack.value ?: return
PopupMenu(
requireContext(),
binding.nowPlayingDetailsInfo,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.track_info)
setOnMenuItemClickListener {
onDetailsMenuItemClickedCb()
when (it.itemId) {
R.id.track_info_artist -> findNavController().navigate(
MainNavDirections.globalBrowseToAlbums(
currentTrack.artist,
currentTrack.album?.cover()
)
)
R.id.track_info_album -> currentTrack.album?.let { album ->
findNavController().navigate(MainNavDirections.globalBrowseTracks(album))
}
R.id.track_info_details -> TrackInfoDetailsFragment.new(currentTrack).show(
requireActivity().supportFragmentManager, "dialog"
)
}
true
}
show()
}
}
private fun refreshCurrentTrack(track: Track?) {
viewModel.currentTrack.postValue(track)
val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0)
viewModel.repeatMode.postValue(cachedRepeatMode % 3)
// At this point, a non-null track is required
if (track == null) return
favoritedRepository.fetch().untilNetwork(lifecycleScope, Dispatchers.IO) { favorites, _, _, _ ->
lifecycleScope.launch(Dispatchers.Main) {
track.favorite = favorites.contains(track.id)
// Trigger UI refresh
viewModel.currentTrack.postValue(viewModel.currentTrack.value)
}
}
}
inner class OnSeekBarChanged : OnSeekBarChangeListener {
override fun onStopTrackingTouch(view: SeekBar?) {}
override fun onStartTrackingTouch(view: SeekBar?) {}
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
CommandBus.send(Command.Seek(progress))
}
}
}
}