kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale-android
Improve player bottom sheet, in particular fling support
rodzic
6472a3743e
commit
45773aac8d
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<String>" />
|
||||
<variable name="isCurrentTrackFavorite" type="LiveData<Boolean>" />
|
||||
<variable name="repeatModeResource" type="LiveData<Drawable>" />
|
||||
<variable name="repeatModeAlpha" type="LiveData<Float>" />
|
||||
<variable name="currentProgressText" type="LiveData<String>" />
|
||||
<variable name="currentDurationText" type="LiveData<String>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<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>
|
|
@ -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<Boolean>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<Integer>" />
|
||||
<variable name="currentTrackTitle" type="LiveData<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue