package audio.funkwhale.ffa.fragments import android.os.Bundle 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.distinctUntilChanged import androidx.lifecycle.lifecycleScope 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.BottomSheetIneractable 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 jp.wasabeef.picasso.transformations.RoundedCornersTransformation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) { private val binding by lazy { FragmentNowPlayingBinding.bind(requireView()) } private val viewModel by viewModels() private val favoriteRepository by lazy { FavoritesRepository(requireContext()) } private val favoritedRepository by lazy { FavoritedRepository(requireContext()) } private val bottomSheet: BottomSheetIneractable? by lazy { var view = this.view?.parent while (view != null) { if(view is BottomSheetIneractable) return@lazy view view = view.parent } null } 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() } } with(binding.header) { 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) } } binding.nowPlayingDetailsInfo.setOnClickListener { openInfoMenu() } lifecycleScope.launch(Dispatchers.Main) { CommandBus.get().collect { onCommand(it) } } lifecycleScope.launch(Dispatchers.Main) { EventBus.get().collect { onEvent(it) } } lifecycleScope.launch(Dispatchers.Main) { ProgressBus.get().collect { onProgress(it) } } } 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 onEvent(event: Event): Unit = when (event) { is Event.Buffering -> viewModel.isBuffering.postValue(event.value) is Event.StateChanged -> viewModel.isPlaying.postValue(event.playing) 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) { 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) binding.nowPlayingDetailCover.setImageResource(R.drawable.cover) return } CoverArt.withContext(requireContext(), maybeNormalizeUrl(track.album?.cover())) .fit() .centerCrop() .into(binding.nowPlayingDetailCover) CoverArt.withContext(requireContext(), maybeNormalizeUrl(track.album?.cover())) .fit() .centerCrop() .transform(RoundedCornersTransformation(16, 0)) .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 { bottomSheet?.close() 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)) } } } }