Improve player bottom sheet, in particular fling support

This commit is contained in:
Christophe Henry 2023-01-09 09:39:19 +01:00
parent 6472a3743e
commit 45773aac8d
25 changed files with 1094 additions and 1312 deletions

View File

@ -7,6 +7,7 @@ plugins {
id("kotlin-android")
id("androidx.navigation.safeargs.kotlin")
id("kotlin-parcelize")
id("kotlin-kapt")
id("org.jlleitschuh.gradle.ktlint") version "11.2.0"
id("com.gladed.androidgitversion") version "0.4.14"

View File

@ -22,13 +22,11 @@
android:name=".activities.SplashActivity"
android:launchMode="singleInstance"
android:noHistory="true"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
@ -40,9 +38,7 @@
android:launchMode="singleInstance"
android:screenOrientation="portrait" />
<activity
android:name=".activities.MainActivity"
android:screenOrientation="portrait" />
<activity android:name=".activities.MainActivity" />
<activity
android:name=".activities.DownloadsActivity"

View File

@ -34,6 +34,7 @@ import audio.funkwhale.ffa.databinding.ActivityMainBinding
import audio.funkwhale.ffa.fragments.AddToPlaylistDialog
import audio.funkwhale.ffa.fragments.BrowseFragmentDirections
import audio.funkwhale.ffa.fragments.LandscapeQueueFragment
import audio.funkwhale.ffa.fragments.NowPlayingFragment
import audio.funkwhale.ffa.fragments.QueueFragment
import audio.funkwhale.ffa.fragments.TrackInfoDetailsFragment
import audio.funkwhale.ffa.model.Track
@ -67,6 +68,9 @@ import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.offline.DownloadService
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.gson.Gson
import com.preference.PowerPreference
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
@ -82,7 +86,6 @@ class MainActivity : AppCompatActivity() {
LOGOUT(1001)
}
private val favoriteRepository = FavoritesRepository(this)
private val favoritedRepository = FavoritedRepository(this)
private var menu: Menu? = null
@ -104,8 +107,8 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(binding.appbar)
onBackPressedDispatcher.addCallback(this) {
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
if (binding.nowPlayingBottomSheet.isOpen) {
binding.nowPlayingBottomSheet.close()
} else {
navigation.navigateUp()
}
@ -121,66 +124,16 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
findViewById<DisableableFrameLayout?>(R.id.container)?.apply {
setShouldRegisterTouch {
if (binding.nowPlaying.isOpened()) {
binding.nowPlaying.close()
false
} else {
true
}
}
}
binding.nowPlaying.getFragment<NowPlayingFragment>().apply {
favoritedRepository.update(requireContext(), lifecycleScope)
favoritedRepository.update(this, lifecycleScope)
startService(Intent(requireContext(), PlayerService::class.java))
DownloadService.start(requireContext(), PinService::class.java)
startService(Intent(this, PlayerService::class.java))
DownloadService.start(this, PinService::class.java)
CommandBus.send(Command.RefreshService)
CommandBus.send(Command.RefreshService)
lifecycleScope.launch(IO) {
Userinfo.get(this@MainActivity, oAuth)
}
with(binding) {
nowPlayingContainer?.nowPlayingToggle?.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
nowPlayingContainer?.nowPlayingNext?.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
nowPlayingContainer?.nowPlayingDetailsPrevious?.setOnClickListener {
CommandBus.send(Command.PreviousTrack)
}
nowPlayingContainer?.nowPlayingDetailsNext?.setOnClickListener {
CommandBus.send(Command.NextTrack)
}
nowPlayingContainer?.nowPlayingDetailsToggle?.setOnClickListener {
CommandBus.send(Command.ToggleState)
}
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.setOnSeekBarChangeListener(
object : SeekBar.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))
}
}
})
landscapeQueue?.let {
supportFragmentManager.beginTransaction()
.replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
lifecycleScope.launch(IO) {
Userinfo.get(this@MainActivity, oAuth)
}
}
}
@ -223,7 +176,7 @@ class MainActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
binding.nowPlaying.close()
binding.nowPlayingBottomSheet.close()
navigation.popBackStack(R.id.browseFragment, false)
}
@ -298,70 +251,22 @@ class MainActivity : AppCompatActivity() {
private fun watchEventBus() {
lifecycleScope.launch(Main) {
EventBus.get().collect { event ->
if (event is Event.LogOut) {
FFA.get().deleteAllData(this@MainActivity)
startActivity(
Intent(this@MainActivity, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
}
)
finish()
} else if (event is Event.PlaybackError) {
toast(event.message)
} else if (event is Event.Buffering) {
when (event.value) {
true -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.VISIBLE
false -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.GONE
}
} else if (event is Event.PlaybackStopped) {
if (binding.nowPlaying.visibility == View.VISIBLE) {
(binding.navHostFragment.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2
}
binding.landscapeQueue?.let { landscape_queue ->
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
it.bottomMargin = it.bottomMargin / 2
when(event) {
is Event.LogOut -> logout()
is Event.PlaybackError -> toast(event.message)
is Event.PlaybackStopped -> binding.nowPlayingBottomSheet.hide()
is Event.TrackFinished -> incrementListenCount(event.track)
is Event.QueueChanged -> {
if(binding.nowPlayingBottomSheet.isHidden) binding.nowPlayingBottomSheet.show()
findViewById<View>(R.id.nav_queue)?.let { view ->
ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let {
it.duration = 500
it.interpolator = AccelerateDecelerateInterpolator()
it.start()
}
}
binding.nowPlaying.animate()
.alpha(0.0f)
.setDuration(400)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animator: Animator) {
binding.nowPlaying.visibility = View.GONE
}
})
.start()
}
} else if (event is Event.TrackFinished) {
incrementListenCount(event.track)
} else if (event is Event.StateChanged) {
when (event.playing) {
true -> {
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
}
false -> {
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
}
}
} else if (event is Event.QueueChanged) {
findViewById<View>(R.id.nav_queue)?.let { view ->
ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let {
it.duration = 500
it.interpolator = AccelerateDecelerateInterpolator()
it.start()
}
}
else -> {}
}
}
}
@ -402,24 +307,6 @@ class MainActivity : AppCompatActivity() {
}
}
}
lifecycleScope.launch(Main) {
ProgressBus.get().collect { (current, duration, percent) ->
binding.nowPlayingContainer?.nowPlayingProgress?.progress = percent
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.progress = percent
val currentMins = (current / 1000) / 60
val currentSecs = (current / 1000) % 60
val durationMins = duration / 60
val durationSecs = duration % 60
binding.nowPlayingContainer?.nowPlayingDetailsProgressCurrent?.text =
"%02d:%02d".format(currentMins, currentSecs)
binding.nowPlayingContainer?.nowPlayingDetailsProgressDuration?.text =
"%02d:%02d".format(durationMins, durationSecs)
}
}
}
private fun refreshCurrentTrack(track: Track?) {
@ -444,175 +331,6 @@ class MainActivity : AppCompatActivity() {
}
}
}
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
val lic = this.layoutInflater.context
CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
.fit()
.centerCrop()
.into(binding.nowPlayingContainer?.nowPlayingCover)
binding.nowPlayingContainer?.nowPlayingDetailsCover?.let { nowPlayingDetailsCover ->
CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
.into(nowPlayingDetailsCover)
}
if (binding.nowPlayingContainer?.nowPlayingCover == null) {
lifecycleScope.launch(Default) {
val width = DisplayMetrics().apply {
windowManager.defaultDisplay.getMetrics(this)
}.widthPixels
val backgroundCover = CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
.get()
.run { Bitmap.createScaledBitmap(this, width, width, false).toDrawable(resources) }
.apply {
alpha = 20
gravity = Gravity.CENTER
}
withContext(Main) {
binding.nowPlayingContainer?.nowPlayingDetails?.background = backgroundCover
}
}
}
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.let { now_playing_details_repeat ->
changeRepeatMode(FFACache.getLine(this@MainActivity, "repeat")?.toInt() ?: 0)
now_playing_details_repeat.setOnClickListener {
val current = FFACache.getLine(this@MainActivity, "repeat")?.toInt() ?: 0
changeRepeatMode((current + 1) % 3)
}
}
binding.nowPlayingContainer?.nowPlayingDetailsInfo?.let { nowPlayingDetailsInfo ->
nowPlayingDetailsInfo.setOnClickListener {
PopupMenu(
this@MainActivity,
nowPlayingDetailsInfo,
Gravity.START,
R.attr.actionOverflowMenuStyle,
0
).apply {
inflate(R.menu.track_info)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_info_artist -> BrowseFragmentDirections.browseToAlbums(
track.artist,
track.album?.cover()
)
R.id.track_info_album -> track.album?.let(BrowseFragmentDirections::browseToTracks)
R.id.track_info_details -> TrackInfoDetailsFragment.new(track)
.show(supportFragmentManager, "dialog")
}
binding.nowPlaying.close()
true
}
show()
}
}
}
binding.nowPlayingContainer?.nowPlayingDetailsFavorite?.let { now_playing_details_favorite ->
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id)
when (track.favorite) {
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
}
}
}
now_playing_details_favorite.setOnClickListener {
when (track.favorite) {
true -> {
favoriteRepository.deleteFavorite(track.id)
now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
}
false -> {
favoriteRepository.addFavorite(track.id)
now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
}
}
track.favorite = !track.favorite
favoriteRepository.fetch(Repository.Origin.Network.origin)
}
binding.nowPlayingContainer?.nowPlayingDetailsAddToPlaylist?.setOnClickListener {
CommandBus.send(Command.AddToPlaylist(listOf(track)))
}
}
}
}
private fun changeRepeatMode(index: Int) {
when (index) {
// From no repeat to repeat all
0 -> {
FFACache.set(this@MainActivity, "repeat", "0")
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 0.2f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF))
}
// From repeat all to repeat one
1 -> {
FFACache.set(this@MainActivity, "repeat", "1")
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL))
}
// From repeat one to no repeat
2 -> {
FFACache.set(this@MainActivity, "repeat", "2")
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
ContextCompat.getColor(
this,
R.color.controlForeground
)
)
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ONE))
}
}
}
@ -633,4 +351,15 @@ class MainActivity : AppCompatActivity() {
}
}
}
private fun logout() {
FFA.get().deleteAllData(this@MainActivity)
startActivity(
Intent(this@MainActivity, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
}
)
finish()
}
}

View File

@ -41,7 +41,6 @@ class AlbumsGridAdapter(
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(album.cover()))
.fit()
.placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0))
.into(holder.cover)

View File

@ -9,29 +9,16 @@ import audio.funkwhale.ffa.fragments.FavoritesFragment
import audio.funkwhale.ffa.fragments.PlaylistsFragment
import audio.funkwhale.ffa.fragments.RadiosFragment
class BrowseTabsAdapter(val context: Fragment) :
FragmentStateAdapter(context) {
var tabs = mutableListOf<Fragment>()
class BrowseTabsAdapter(val context: Fragment) : FragmentStateAdapter(context) {
override fun getItemCount() = 5
override fun createFragment(position: Int): Fragment {
tabs.getOrNull(position)?.let {
return it
}
val fragment = when (position) {
0 -> ArtistsFragment()
1 -> AlbumsGridFragment()
2 -> PlaylistsFragment()
3 -> RadiosFragment()
4 -> FavoritesFragment()
else -> ArtistsFragment()
}
tabs.add(position, fragment)
return fragment
override fun createFragment(position: Int): Fragment = when (position) {
0 -> ArtistsFragment()
1 -> AlbumsGridFragment()
2 -> PlaylistsFragment()
3 -> RadiosFragment()
4 -> FavoritesFragment()
else -> ArtistsFragment()
}
fun tabText(position: Int): String {

View File

@ -0,0 +1,250 @@
package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.PopupMenu
import androidx.customview.widget.Openable
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.*
import audio.funkwhale.ffa.viewmodel.NowPlayingViewModel
import audio.funkwhale.ffa.views.NowPlayingBottomSheet
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<NowPlayingViewModel>()
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<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)
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))
}
}
}
}

View File

@ -0,0 +1,10 @@
package audio.funkwhale.ffa.utils
import androidx.customview.widget.Openable
interface BottomSheetIneractable: Openable {
val isHidden: Boolean
fun show()
fun hide()
fun toggle()
}

View File

@ -2,7 +2,9 @@ package audio.funkwhale.ffa.utils
import android.content.Context
import android.net.Uri
import android.transition.CircularPropagation
import android.util.Log
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import audio.funkwhale.ffa.BuildConfig
import audio.funkwhale.ffa.R
import com.squareup.picasso.Downloader
@ -252,9 +254,10 @@ open class CoverArt private constructor() {
* The primary entrypoint for the codebase.
*/
fun withContext(context: Context, url: String?): RequestCreator {
return buildPicasso(context)
.load(url)
.placeholder(R.drawable.cover)
val request = buildPicasso(context).load(url)
if(url == null) request.placeholder(R.drawable.cover)
else request.placeholder(CircularProgressDrawable(context))
return request.error(R.drawable.cover)
}
}
}

View File

@ -149,3 +149,5 @@ inline fun <T, U, V, W, R> LiveData<T>.mergeWith(
}
}
}
public fun String?.toIntOrElse(default: Int): Int = this?.toIntOrNull(radix = 10) ?: default

View File

@ -0,0 +1,25 @@
package audio.funkwhale.ffa.utils
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageButton
import androidx.annotation.ColorRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.databinding.BindingAdapter
@BindingAdapter("srcCompat")
fun setImageViewResource(imageView: AppCompatImageView, resource: Any?) = when (resource) {
is Bitmap -> imageView.setImageBitmap(resource)
is Int -> imageView.setImageResource(resource)
is Drawable -> imageView.setImageDrawable(resource)
else -> imageView.setImageDrawable(ColorDrawable(Color.TRANSPARENT))
}
@BindingAdapter("tint")
fun setTint(imageView: ImageButton, @ColorRes resource: Int) = resource.let {
imageView.setColorFilter(resource)
}

View File

@ -0,0 +1,56 @@
package audio.funkwhale.ffa.viewmodel
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.google.android.exoplayer2.Player
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
class NowPlayingViewModel(app: Application) : AndroidViewModel(app) {
val isBuffering = MutableLiveData(false)
val isPlaying = MutableLiveData(false)
val repeatMode = MutableLiveData(0)
val progress = MutableLiveData(0)
val currentTrack = MutableLiveData<Track?>(null)
val currentProgressText = MutableLiveData("")
val currentDurationText = MutableLiveData("")
// Calling distinctUntilChanged() prevents triggering an event when the track hasn't changed
val currentTrackTitle = currentTrack.distinctUntilChanged().map { it?.title ?: "" }
val currentTrackArtist = currentTrack.distinctUntilChanged().map { it?.artist?.name ?: "" }
// Not calling distinctUntilChanged() here as we need to process every event
val isCurrentTrackFavorite = currentTrack.map {
it?.favorite ?: false
}
val repeatModeResource = repeatMode.distinctUntilChanged().map {
when (it) {
Player.REPEAT_MODE_ONE -> AppCompatResources.getDrawable(context, R.drawable.repeat_one)
else -> AppCompatResources.getDrawable(context, R.drawable.repeat)
}
}
val repeatModeAlpha = repeatMode.distinctUntilChanged().map {
when (it) {
Player.REPEAT_MODE_OFF -> 0.2f
else -> 1f
}
}
private val context: Context
get() = getApplication<FFA>().applicationContext
}

View File

@ -0,0 +1,67 @@
package audio.funkwhale.ffa.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.BottomSheetIneractable
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.card.MaterialCardView
class NowPlayingBottomSheet @JvmOverloads constructor (
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs), BottomSheetIneractable {
val behavior = BottomSheetBehavior<NowPlayingBottomSheet>(context, attrs)
private val targetHeaderId: Int
init {
context.theme.obtainStyledAttributes(attrs, R.styleable.NowPlaying, defStyleAttr, 0).use {
targetHeaderId = it.getResourceId(R.styleable.NowPlaying_target_header, NO_ID)
}
}
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
super.setLayoutParams(params)
(params as CoordinatorLayout.LayoutParams).behavior = behavior
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
findViewById<View>(targetHeaderId)?.apply {
behavior.setPeekHeight(this.measuredHeight, false)
this.setOnClickListener { this@NowPlayingBottomSheet.toggle() }
} ?: hide()
}
// Bottom sheet interactions
override val isHidden: Boolean get() = behavior.state == BottomSheetBehavior.STATE_HIDDEN
override fun isOpen(): Boolean = behavior.state == BottomSheetBehavior.STATE_EXPANDED
override fun open() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun close() {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
override fun show() {
behavior.isHideable = false
close()
}
override fun hide() {
behavior.isHideable = true
behavior.state = BottomSheetBehavior.STATE_HIDDEN
}
override fun toggle() {
if (isHidden) return
if (isOpen) close() else open()
}
}

View File

@ -1,255 +0,0 @@
package audio.funkwhale.ffa.views
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.PartialNowPlayingBinding
import com.google.android.material.card.MaterialCardView
import kotlin.math.abs
import kotlin.math.min
class NowPlayingView : MaterialCardView {
val activity: Context
var gestureDetector: GestureDetector? = null
var gestureDetectorCallback: OnGestureDetection? = null
private val binding =
PartialNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
constructor(context: Context) : super(context) {
activity = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
activity = context
}
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style) {
activity = context
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
binding.nowPlayingRoot.measure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)
)
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility == View.VISIBLE && gestureDetector == null) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
gestureDetectorCallback = OnGestureDetection()
gestureDetector = GestureDetector(context, gestureDetectorCallback!!)
setOnTouchListener { _, motionEvent ->
val ret = gestureDetector?.onTouchEvent(motionEvent) ?: false
if (motionEvent.actionMasked == MotionEvent.ACTION_UP) {
if (gestureDetectorCallback?.isScrolling == true) {
gestureDetectorCallback?.onUp()
}
}
performClick()
ret
}
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
}
fun isOpened(): Boolean = gestureDetectorCallback?.isOpened() ?: false
fun close() {
gestureDetectorCallback?.close()
}
inner class OnGestureDetection : GestureDetector.SimpleOnGestureListener() {
private var maxHeight = 0
private var minHeight = 0
private var maxMargin = 0
private var initialTouchY = 0f
private var lastTouchY = 0f
var isScrolling = false
private var flingAnimator: ValueAnimator? = null
init {
(layoutParams as? MarginLayoutParams)?.let {
maxMargin = it.marginStart
}
minHeight = TypedValue().let {
activity.theme.resolveAttribute(R.attr.actionBarSize, it, true)
TypedValue.complexToDimensionPixelSize(it.data, resources.displayMetrics)
}
maxHeight = binding.nowPlayingDetails.measuredHeight + (2 * maxMargin)
}
override fun onDown(e: MotionEvent): Boolean {
initialTouchY = e.rawY
lastTouchY = e.rawY
return true
}
fun onUp(): Boolean {
isScrolling = false
layoutParams.let {
val offsetToMax = maxHeight - height
val offsetToMin = height - minHeight
flingAnimator =
if (offsetToMin < offsetToMax) ValueAnimator.ofInt(it.height, minHeight)
else ValueAnimator.ofInt(it.height, maxHeight)
animateFling(500)
return true
}
}
override fun onFling(
firstMotionEvent: MotionEvent,
secondMotionEvent: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
isScrolling = false
layoutParams.let {
val diff =
if (velocityY < 0) maxHeight - it.height
else it.height - minHeight
flingAnimator =
if (velocityY < 0) ValueAnimator.ofInt(it.height, maxHeight)
else ValueAnimator.ofInt(it.height, minHeight)
animateFling(min(abs((diff.toFloat() / velocityY * 1000).toLong()), 600))
}
return true
}
override fun onScroll(
firstMotionEvent: MotionEvent,
secondMotionEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
isScrolling = true
layoutParams.let {
val newHeight = it.height + lastTouchY - secondMotionEvent.rawY
val progress = (newHeight - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress)
(layoutParams as? MarginLayoutParams)?.let { params ->
params.marginStart = newMargin.toInt()
params.marginEnd = newMargin.toInt()
params.bottomMargin = newMargin.toInt()
}
layoutParams = layoutParams.apply {
when {
newHeight <= minHeight -> {
height = minHeight
return true
}
newHeight >= maxHeight -> {
height = maxHeight
return true
}
else -> height = newHeight.toInt()
}
}
binding.summary.alpha = 1f - progress
binding.summary.layoutParams = binding.summary.layoutParams.apply {
height = (minHeight * (1f - progress)).toInt()
}
}
lastTouchY = secondMotionEvent.rawY
return true
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
layoutParams.let {
if (height != minHeight) return true
flingAnimator = ValueAnimator.ofInt(it.height, maxHeight)
animateFling(300)
}
return true
}
fun isOpened(): Boolean = layoutParams.height == maxHeight
fun close(): Boolean {
layoutParams.let {
if (it.height == minHeight) return true
flingAnimator = ValueAnimator.ofInt(it.height, minHeight)
animateFling(300)
}
return true
}
private fun animateFling(dur: Long) {
flingAnimator?.apply {
duration = dur
interpolator = DecelerateInterpolator()
addUpdateListener { valueAnimator ->
layoutParams = layoutParams.apply {
val newHeight = valueAnimator.animatedValue as Int
val progress = (newHeight.toFloat() - minHeight) / (maxHeight - minHeight)
val newMargin = maxMargin - (maxMargin * progress)
(layoutParams as? MarginLayoutParams)?.let {
it.marginStart = newMargin.toInt()
it.marginEnd = newMargin.toInt()
it.bottomMargin = newMargin.toInt()
}
height = newHeight
binding.summary.alpha = 1f - progress
binding.summary.layoutParams = binding.summary.layoutParams.apply {
height = (minHeight * (1f - progress)).toInt()
}
}
}
start()
}
}
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
class SquareImageView : AppCompatImageView {
open class SquareImageView : AppCompatImageView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style)
@ -12,6 +12,8 @@ class SquareImageView : AppCompatImageView {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredWidth)
val dimension = if(measuredWidth == 0 && measuredHeight > 0) measuredHeight else measuredWidth
setMeasuredDimension(dimension, dimension)
}
}

View File

@ -1,64 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
android:layout_height="0dp"
android:background="@color/surface"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_weight="10">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<androidx.fragment.app.FragmentContainerView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:navGraph="@navigation/main_nav"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
tools:layout="@layout/fragment_artists" />
<FrameLayout
android:id="@+id/landscape_queue"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
android:layout_weight="1"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/landscape_queue"
android:name="audio.funkwhale.ffa.fragments.LandscapeQueueFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:layout="@layout/partial_queue" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
<audio.funkwhale.ffa.views.NowPlayingView
android:id="@+id/now_playing"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:alpha="0"
android:visibility="gone"
app:cardCornerRadius="8dp"
app:cardElevation="12dp"
app:layout_dodgeInsetEdges="bottom"
tools:alpha="1"
tools:visibility="visible">
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/appbar_wrapper"
app:layout_constraintTop_toTopOf="parent">
<audio.funkwhale.ffa.views.NowPlayingBottomSheet
android:id="@+id/now_playing_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="3dp"
app:cardElevation="12dp"
app:target_header="@id/header">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/now_playing"
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_now_playing" />
</audio.funkwhale.ffa.views.NowPlayingBottomSheet>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/partial_now_playing" />
</audio.funkwhale.ffa.views.NowPlayingView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/appbar"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/appbar_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:theme="@style/AppTheme.AppBar"
app:backgroundTint="@color/colorPrimaryDark"
app:layout_insetEdge="bottom"
app:navigationIcon="@drawable/funkwhaleshape"
tools:menu="@menu/toolbar" />
app:layout_constraintBottom_toBottomOf="parent">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:theme="@style/AppTheme.AppBar"
app:backgroundTint="@color/elevatedSurface"
app:layout_insetEdge="bottom"
app:navigationIcon="@drawable/funkwhaleshape"
tools:menu="@menu/toolbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/elevatedSurface">
<include
android:id="@+id/header"
layout="@layout/partial_now_playing_header" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cover_container"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/now_playing_detail_cover"
android:layout_width="0dp"
android:layout_height="match_parent"
android:scaleType="fitCenter"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/cover"
tools:src="@tools:sample/avatars" />
<ImageButton
android:id="@+id/now_playing_details_info"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more"
app:layout_constraintEnd_toEndOf="@id/now_playing_detail_cover"
app:layout_constraintTop_toTopOf="@id/now_playing_detail_cover"
app:tint="@color/controlForeground" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/controls"
layout="@layout/partial_now_playing_controls"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cover_container"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -1,251 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/elevatedSurface"
android:orientation="vertical">
<LinearLayout
android:id="@+id/summary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:orientation="vertical">
<ProgressBar
android:id="@+id/now_playing_progress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-6dp"
android:layout_marginBottom="-6dp"
android:progress="40"
android:progressTint="@color/colorPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_marginEnd="16dp">
<audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/now_playing_cover"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
tools:src="@tools:sample/avatars" />
<ProgressBar
android:id="@+id/now_playing_buffering"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:indeterminate="true"
android:indeterminateTint="@color/controlForeground"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_album"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="?attr/actionBarSize"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
app:icon="@drawable/play" />
<ImageButton
android:id="@+id/now_playing_next"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/now_playing_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_details_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
</LinearLayout>
<ImageButton
android:id="@+id/now_playing_details_add_to_playlist"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/add_to_playlist" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/favorite" />
<ImageButton
android:id="@+id/now_playing_details_info"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more"
app:tint="@color/controlForeground" />
</LinearLayout>
<SeekBar
android:id="@+id/now_playing_details_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:max="100"
android:progressBackgroundTint="#cacaca"
android:progressTint="@color/controlForeground"
android:thumbOffset="3dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:thumbTint="@color/controlForeground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/now_playing_details_progress_current"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/now_playing_details_progress_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/now_playing_details_previous"
style="@style/IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/control_previous"
android:src="@drawable/previous" />
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_details_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="64dp"
android:layout_height="64dp"
app:cornerRadius="64dp"
app:icon="@drawable/play"
app:iconSize="32dp" />
<ImageButton
android:id="@+id/now_playing_details_next"
style="@style/IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/now_playing_details_repeat"
style="@style/IconButton"
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/control_next"
android:src="@drawable/repeat" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,50 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/surface">
android:layout_height="0dp"
android:background="@color/surface"
app:layout_constraintVertical_weight="10"
app:layout_constraintTop_toTopOf="parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true"
app:navGraph="@navigation/main_nav"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:navGraph="@navigation/main_nav"
tools:layout="@layout/fragment_artists" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<audio.funkwhale.ffa.views.NowPlayingView
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/appbar_wrapper">
<audio.funkwhale.ffa.views.NowPlayingBottomSheet
android:id="@+id/now_playing_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="3dp"
app:cardElevation="12dp"
app:target_header="@id/header">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/now_playing"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:alpha="0"
android:visibility="gone"
app:cardCornerRadius="3dp"
app:cardElevation="12dp"
app:layout_dodgeInsetEdges="bottom"
tools:alpha="1"
tools:visibility="visible">
android:layout_height="match_parent"
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"
tools:layout="@layout/fragment_now_playing"/>
</audio.funkwhale.ffa.views.NowPlayingBottomSheet>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include
android:id="@+id/now_playing_container"
layout="@layout/partial_now_playing" />
</audio.funkwhale.ffa.views.NowPlayingView>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/appbar_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:theme="@style/AppTheme.AppBar"
app:backgroundTint="@color/elevatedSurface"
app:layout_insetEdge="bottom"
app:navigationIcon="@drawable/funkwhaleshape"
tools:menu="@menu/toolbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:theme="@style/AppTheme.AppBar"
app:backgroundTint="@color/elevatedSurface"
app:layout_insetEdge="bottom"
app:navigationIcon="@drawable/funkwhaleshape"
tools:menu="@menu/toolbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/elevatedSurface">
<include
android:id="@+id/header"
layout="@layout/partial_now_playing_header" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cover_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/controls">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/now_playing_detail_cover"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/cover"
tools:src="@tools:sample/avatars" />
<ImageButton
android:id="@+id/now_playing_details_info"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="@id/now_playing_detail_cover"
app:layout_constraintTop_toTopOf="@id/now_playing_detail_cover"
style="@style/IconButton"
android:layout_gravity="top|end"
android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more"
app:tint="@color/controlForeground" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/controls"
layout="@layout/partial_now_playing_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -1,283 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/elevatedSurface"
android:orientation="vertical">
<LinearLayout
android:id="@+id/summary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:orientation="vertical">
<ProgressBar
android:id="@+id/now_playing_progress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-6dp"
android:layout_marginBottom="-6dp"
android:progress="40"
android:progressTint="@color/colorPrimaryDark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_marginEnd="16dp">
<audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/now_playing_cover"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
tools:src="@tools:sample/avatars" />
<ProgressBar
android:id="@+id/now_playing_buffering"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:indeterminate="true"
android:indeterminateTint="@color/controlForeground"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_title"
style="@style/AppTheme.ItemTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_album"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
tools:text="Muse" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="?attr/actionBarSize"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
app:icon="@drawable/play" />
<ImageButton
android:id="@+id/now_playing_next"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/now_playing_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp">
<audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/now_playing_details_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/funkwhaleshape"
tools:src="@tools:sample/avatars" />
<ImageButton
android:id="@+id/now_playing_details_info"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more"
app:tint="@color/controlForeground" />
</FrameLayout>
<LinearLayout
android:id="@+id/now_playing_details_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:orientation="vertical"
android:paddingTop="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_details_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
</LinearLayout>
<ImageButton
android:id="@+id/now_playing_details_add_to_playlist"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/playlist_add_to"
android:src="@drawable/add_to_playlist" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/favorite" />
</LinearLayout>
<SeekBar
android:id="@+id/now_playing_details_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:max="100"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:progressBackgroundTint="#cacaca"
android:progressTint="@color/controlForeground"
android:thumbOffset="3dp"
android:thumbTint="@color/controlForeground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/now_playing_details_progress_current"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/now_playing_details_progress_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/now_playing_details_previous"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/control_previous"
android:src="@drawable/previous" />
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_details_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="64dp"
android:layout_height="64dp"
app:cornerRadius="64dp"
app:icon="@drawable/play"
app:iconSize="32dp" />
<ImageButton
android:id="@+id/now_playing_details_next"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/now_playing_details_repeat"
style="@style/IconButton"
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/control_next"
android:src="@drawable/repeat" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="androidx.lifecycle.LiveData" />
<import type="android.graphics.drawable.Drawable" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;String>" />
<variable name="isCurrentTrackFavorite" type="LiveData&lt;Boolean>" />
<variable name="repeatModeResource" type="LiveData&lt;Drawable>" />
<variable name="repeatModeAlpha" type="LiveData&lt;Float>" />
<variable name="currentProgressText" type="LiveData&lt;String>" />
<variable name="currentDurationText" type="LiveData&lt;String>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/current_playing_details_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:text="@{currentTrackTitle}"
android:textColor="@color/itemTitle"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_add_to_playlist"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/current_playing_details_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:text="@{currentTrackArtist}"
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_add_to_playlist"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/current_playing_details_title"
tools:text="Muse" />
<ImageButton
android:id="@+id/now_playing_details_add_to_playlist"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/playlist_add_to"
android:src="@drawable/add_to_playlist"
app:layout_constraintBottom_toBottomOf="@+id/current_playing_details_artist"
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_favorite"
app:layout_constraintTop_toTopOf="@+id/current_playing_details_title" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/control_add_to_favorties"
android:src="@drawable/favorite"
app:layout_constraintBottom_toBottomOf="@+id/current_playing_details_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/current_playing_details_title"
app:tint="@{isCurrentTrackFavorite ? @color/colorFavorite : @color/controlForeground, default=@color/controlForeground}" />
<TextView
android:id="@+id/now_playing_details_progress_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{currentProgressText, default="5:04"}'
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_progress"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/now_playing_details_progress" />
<SeekBar
android:id="@+id/now_playing_details_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="100"
android:layout_margin="8dp"
android:progress="@{progress, default=40}"
android:progressBackgroundTint="#cacaca"
android:progressTint="@color/controlForeground"
android:thumbOffset="3dp"
android:thumbTint="@color/controlForeground"
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_progress_duration"
app:layout_constraintStart_toEndOf="@+id/now_playing_details_progress_current"
app:layout_constraintTop_toBottomOf="@+id/current_playing_details_artist" />
<TextView
android:id="@+id/now_playing_details_progress_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{currentDurationText, default="5:04"}'
android:textAlignment="textEnd"
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/now_playing_details_progress" />
<ImageButton
android:id="@+id/now_playing_details_previous"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
android:contentDescription="@string/control_previous"
android:src="@drawable/previous"
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_toggle"
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_details_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_margin="8dp"
app:cornerRadius="64dp"
app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}"
app:iconSize="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
<ImageButton
android:id="@+id/now_playing_details_next"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next"
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
app:layout_constraintStart_toEndOf="@+id/now_playing_details_toggle"
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
<ImageButton
android:id="@+id/now_playing_details_repeat"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
android:alpha="@{repeatModeAlpha, default=1}"
android:contentDescription="@string/control_repeat_mode"
android:src="@{repeatModeResource, default=@drawable/repeat}"
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress"
app:tint="@color/controlForeground" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="androidx.lifecycle.LiveData" />
<import type="android.view.View" />
<import type="android.graphics.drawable.Drawable" />
<variable name="isBuffering" type="LiveData&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;String>" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/now_playing_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:progress="@{progress, default=40}"
android:progressTint="@color/colorPrimaryDark" />
<audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/now_playing_cover"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/now_playing_progress"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/cover"
tools:src="@tools:sample/avatars" />
<ProgressBar
android:id="@+id/now_playing_buffering"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="@id/now_playing_cover"
app:layout_constraintTop_toTopOf="@id/now_playing_cover"
app:layout_constraintBottom_toBottomOf="@id/now_playing_cover"
app:layout_constraintEnd_toEndOf="@id/now_playing_cover"
android:indeterminate="true"
android:indeterminateTint="@color/controlForeground"
android:visibility="@{isBuffering ? View.VISIBLE : View.GONE, default=gone}" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_controls"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_weight="10"
app:layout_constraintStart_toEndOf="@id/now_playing_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
app:layout_constraintBottom_toBottomOf="parent"
android:padding="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/now_playing_toggle"
style="@style/AppTheme.ItemTitle"
android:text="@{currentTrackTitle}"
android:ellipsize="end"
android:lines="1"
tools:text="Supermassive Black Hole" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/now_playing_toggle"
android:ellipsize="end"
android:lines="1"
android:text="@{currentTrackArtist}"
tools:text="Muse" />
<com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_toggle"
style="@style/AppTheme.OutlinedButton"
android:layout_width="?attr/actionBarSize"
android:layout_height="match_parent"
app:layout_constraintEnd_toStartOf="@id/now_playing_next"
android:layout_marginEnd="16dp"
app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}" />
<ImageButton
android:id="@+id/now_playing_next"
android:layout_width="32dp"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
style="@style/IconButton"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -1,103 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav"
app:startDestination="@id/browseFragment">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav"
app:startDestination="@id/browseFragment">
<fragment
android:id="@+id/browseFragment"
android:name="audio.funkwhale.ffa.fragments.BrowseFragment"
android:label="BrowseFragment">
<action
android:id="@+id/browseToSearch"
app:destination="@id/searchFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToArtists"
app:destination="@id/artistsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToPlaylistTracks"
app:destination="@id/playlistTracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/playlistTracksFragment"
android:name="audio.funkwhale.ffa.fragments.PlaylistTracksFragment"
android:label="PlaylistTracksFragment" >
<argument
android:name="playlist"
app:argType="audio.funkwhale.ffa.model.Playlist" />
</fragment>
<fragment
android:id="@+id/tracksFragment"
android:name="audio.funkwhale.ffa.fragments.TracksFragment"
android:label="TracksFragment" >
<argument
android:name="album"
app:argType="audio.funkwhale.ffa.model.Album" />
</fragment>
<fragment
android:id="@+id/albumsFragment"
android:name="audio.funkwhale.ffa.fragments.AlbumsFragment"
android:label="AlbumsFragment" >
<argument
android:name="artist"
app:argType="audio.funkwhale.ffa.model.Artist" />
<argument
android:name="cover"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
<action
android:id="@+id/albumsToTracks"
app:destination="@id/tracksFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="audio.funkwhale.ffa.fragments.SearchFragment"
android:label="SearchFragment" >
<action
android:id="@+id/searchToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/searchToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/artistsFragment"
android:name="audio.funkwhale.ffa.fragments.ArtistsFragment"
android:label="ArtistsFragment" />
<fragment
android:id="@+id/browseFragment"
android:name="audio.funkwhale.ffa.fragments.BrowseFragment"
android:label="BrowseFragment">
<action
android:id="@+id/browseToSearch"
app:destination="@id/searchFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToArtists"
app:destination="@id/artistsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/browseToPlaylistTracks"
app:destination="@id/playlistTracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/playlistTracksFragment"
android:name="audio.funkwhale.ffa.fragments.PlaylistTracksFragment"
android:label="PlaylistTracksFragment">
<argument
android:name="playlist"
app:argType="audio.funkwhale.ffa.model.Playlist" />
</fragment>
<fragment
android:id="@+id/tracksFragment"
android:name="audio.funkwhale.ffa.fragments.TracksFragment"
android:label="TracksFragment">
<argument
android:name="album"
app:argType="audio.funkwhale.ffa.model.Album" />
</fragment>
<fragment
android:id="@+id/albumsFragment"
android:name="audio.funkwhale.ffa.fragments.AlbumsFragment"
android:label="AlbumsFragment">
<argument
android:name="artist"
app:argType="audio.funkwhale.ffa.model.Artist" />
<argument
android:name="cover"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<action
android:id="@+id/albumsToTracks"
app:destination="@id/tracksFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="audio.funkwhale.ffa.fragments.SearchFragment"
android:label="SearchFragment">
<action
android:id="@+id/searchToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
<action
android:id="@+id/searchToTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down" />
</fragment>
<fragment
android:id="@+id/artistsFragment"
android:name="audio.funkwhale.ffa.fragments.ArtistsFragment"
android:label="ArtistsFragment" />
<action
android:id="@+id/globalBrowseToAlbums"
app:destination="@id/albumsFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down"
/>
<action
android:id="@+id/globalBrowseTracks"
app:destination="@id/tracksFragment"
app:enterAnim="@anim/slide_up"
app:exitAnim="@anim/delayed_fade_out"
app:popEnterAnim="@anim/none"
app:popExitAnim="@anim/slide_down"
/>
</navigation>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NowPlaying">
<attr name="target_header" format="reference" />
</declare-styleable>
</resources>

View File

@ -81,6 +81,8 @@
<string name="control_toggle">Toggle playback</string>
<string name="control_previous">Previous track</string>
<string name="control_next">Next track</string>
<string name="control_repeat_mode">Repeat mode</string>
<string name="control_add_to_favorties">Add to favorties</string>
<string name="error_playback">This track could not be played</string>
<plurals name="album_count">
<item quantity="one">%d album</item>