Ability to shuffle play all tracks from an artist. Should close #21. Also added animations over long-running operations.
This commit is contained in:
parent
cb43615cb1
commit
c75f2e45f6
|
@ -7,12 +7,19 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.views.LoadingImageView
|
||||
import kotlinx.android.synthetic.main.row_radio.view.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosAdapter(val context: Context?, private val listener: OnRadioClickListener) : FunkwhaleAdapter<Radio, RadiosAdapter.ViewHolder>() {
|
||||
interface OnRadioClickListener {
|
||||
fun onClick(holder: View?, radio: Radio)
|
||||
fun onClick(holder: ViewHolder, radio: Radio)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
@ -31,7 +38,6 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis
|
|||
val radio = data[position]
|
||||
|
||||
holder.art.visibility = View.VISIBLE
|
||||
holder.nativeArt.visibility = View.GONE
|
||||
holder.name.text = radio.name
|
||||
holder.description.text = radio.description
|
||||
|
||||
|
@ -43,24 +49,46 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis
|
|||
}
|
||||
|
||||
icon?.let {
|
||||
holder.art.visibility = View.GONE
|
||||
holder.nativeArt.visibility = View.VISIBLE
|
||||
holder.native = true
|
||||
|
||||
holder.nativeArt.setImageDrawable(context.getDrawable(icon))
|
||||
holder.nativeArt.alpha = 0.7f
|
||||
holder.nativeArt.setColorFilter(context.getColor(R.color.controlForeground))
|
||||
holder.art.setImageDrawable(context.getDrawable(icon))
|
||||
holder.art.alpha = 0.7f
|
||||
holder.art.setColorFilter(context.getColor(R.color.controlForeground))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, private val listener: OnRadioClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
val nativeArt = view.native_art
|
||||
val art = view.art
|
||||
val name = view.name
|
||||
val description = view.description
|
||||
|
||||
var native = false
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(view, data[layoutPosition])
|
||||
listener.onClick(this, data[layoutPosition])
|
||||
}
|
||||
|
||||
fun spin() {
|
||||
context?.let {
|
||||
val originalDrawable = art.drawable
|
||||
val originalColorFilter = art.colorFilter
|
||||
val imageAnimator = LoadingImageView.start(context, art)
|
||||
|
||||
art.setColorFilter(context.getColor(R.color.controlForeground))
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.RadioStarted -> {
|
||||
art.colorFilter = originalColorFilter
|
||||
|
||||
LoadingImageView.stop(context, originalDrawable, art, imageAnimator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,11 +18,18 @@ import com.github.apognu.otter.R
|
|||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.adapters.AlbumsAdapter
|
||||
import com.github.apognu.otter.repositories.AlbumsRepository
|
||||
import com.github.apognu.otter.repositories.ArtistTracksRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.views.LoadingFlotingActionButton
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.android.synthetic.main.fragment_albums.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -30,6 +37,8 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
override val viewRes = R.layout.fragment_albums
|
||||
override val recycler: RecyclerView get() = albums
|
||||
|
||||
lateinit var artistTracksRepository: ArtistTracksRepository
|
||||
|
||||
var artistId = 0
|
||||
var artistName = ""
|
||||
var artistArt = ""
|
||||
|
@ -91,6 +100,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
|
||||
adapter = AlbumsAdapter(context, OnAlbumClickListener())
|
||||
repository = AlbumsRepository(context, artistId)
|
||||
artistTracksRepository = ArtistTracksRepository(context, artistId)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -107,7 +117,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
|
||||
cover_background?.let { background ->
|
||||
activity?.let { activity ->
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
GlobalScope.launch(IO) {
|
||||
val width = DisplayMetrics().apply {
|
||||
activity.windowManager.defaultDisplay.getMetrics(this)
|
||||
}.widthPixels
|
||||
|
@ -130,6 +140,25 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
}
|
||||
|
||||
artist.text = artistName
|
||||
|
||||
play.setOnClickListener {
|
||||
val loaderAnimation = LoadingFlotingActionButton.start(play)
|
||||
|
||||
GlobalScope.launch(IO) {
|
||||
artistTracksRepository.fetch(Repository.Origin.Network.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
.shuffled()
|
||||
.also {
|
||||
CommandBus.send(Command.ReplaceQueue(it))
|
||||
|
||||
withContext(Main) {
|
||||
LoadingFlotingActionButton.stop(play, loaderAnimation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class OnAlbumClickListener : AlbumsAdapter.OnAlbumClickListener {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package com.github.apognu.otter.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.forEach
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.RadiosAdapter
|
||||
import com.github.apognu.otter.repositories.RadiosRepository
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.*
|
||||
import kotlinx.android.synthetic.main.fragment_radios.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosFragment : FunkwhaleFragment<Radio, RadiosAdapter>() {
|
||||
override val viewRes = R.layout.fragment_radios
|
||||
|
@ -23,8 +25,25 @@ class RadiosFragment : FunkwhaleFragment<Radio, RadiosAdapter>() {
|
|||
}
|
||||
|
||||
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
|
||||
override fun onClick(holder: View?, radio: Radio) {
|
||||
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: Radio) {
|
||||
holder.spin()
|
||||
recycler.forEach {
|
||||
it.isEnabled = false
|
||||
it.isClickable = false
|
||||
}
|
||||
|
||||
CommandBus.send(Command.PlayRadio(radio))
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.RadioStarted -> recycler.forEach {
|
||||
it.isEnabled = true
|
||||
it.isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -126,6 +126,8 @@ class RadioPlayer(val context: Context) {
|
|||
withContext(Main) {
|
||||
context.toast(context.getString(R.string.radio_playback_error))
|
||||
}
|
||||
} finally {
|
||||
EventBus.send(Event.RadioStarted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.TracksCache
|
||||
import com.github.apognu.otter.utils.TracksResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
|
||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-artist-${artistId}"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=${artistId}", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
}
|
|
@ -42,6 +42,7 @@ sealed class Event {
|
|||
class RefreshTrack(val track: Track?, val play: Boolean) : Event()
|
||||
class StateChanged(val playing: Boolean) : Event()
|
||||
object QueueChanged : Event()
|
||||
object RadioStarted : Event()
|
||||
}
|
||||
|
||||
sealed class Request(var channel: Channel<Response>? = null) {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.github.apognu.otter.views
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.github.apognu.otter.R
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
||||
object LoadingFlotingActionButton {
|
||||
fun start(button: ExtendedFloatingActionButton): ObjectAnimator {
|
||||
button.isEnabled = false
|
||||
button.setIconResource(R.drawable.fab_spinner)
|
||||
button.shrink()
|
||||
|
||||
return ObjectAnimator.ofFloat(button, View.ROTATION, 0f, 360f).apply {
|
||||
duration = 500
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(button: ExtendedFloatingActionButton, animator: ObjectAnimator) {
|
||||
animator.cancel()
|
||||
|
||||
button.isEnabled = true
|
||||
button.setIconResource(R.drawable.play)
|
||||
button.rotation = 0.0f
|
||||
button.extend()
|
||||
}
|
||||
}
|
||||
|
||||
object LoadingImageView {
|
||||
fun start(context: Context?, image: ImageView): ObjectAnimator? {
|
||||
context?.let {
|
||||
image.isEnabled = false
|
||||
image.setImageDrawable(context.getDrawable(R.drawable.fab_spinner))
|
||||
|
||||
return ObjectAnimator.ofFloat(image, View.ROTATION, 0f, 360f).apply {
|
||||
duration = 500
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun stop(context: Context?, original: Drawable, image: ImageView, animator: ObjectAnimator?) {
|
||||
context?.let {
|
||||
animator?.cancel()
|
||||
|
||||
image.isEnabled = true
|
||||
image.setImageDrawable(original)
|
||||
image.rotation = 0.0f
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:id="@android:id/background">
|
||||
<rotate
|
||||
android:fromDegrees="0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toDegrees="360">
|
||||
|
||||
<shape
|
||||
android:shape="ring"
|
||||
android:thickness="3dp"
|
||||
android:type="sweep"
|
||||
android:useLevel="false">
|
||||
|
||||
<gradient
|
||||
android:angle="0"
|
||||
android:centerColor="#00ffffff"
|
||||
android:endColor="#00ffffff"
|
||||
android:startColor="#ffffffff"
|
||||
android:type="sweep"
|
||||
android:useLevel="false" />
|
||||
|
||||
</shape>
|
||||
|
||||
</rotate>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
|
@ -39,6 +39,40 @@
|
|||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:contentDescription="@string/alt_artist_art"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/colorPrimary"
|
||||
android:elevation="10dp"
|
||||
android:text="@string/playback_shuffle"
|
||||
android:textColor="@android:color/white"
|
||||
app:icon="@drawable/play"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cover"
|
||||
app:layout_constraintLeft_toLeftOf="@id/cover"
|
||||
app:layout_constraintRight_toRightOf="@id/cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/cover" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -53,13 +53,33 @@
|
|||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/colorPrimary"
|
||||
android:elevation="10dp"
|
||||
android:text="@string/playback_shuffle"
|
||||
android:textColor="@android:color/white"
|
||||
app:icon="@drawable/play"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cover"
|
||||
app:layout_constraintLeft_toLeftOf="@id/cover"
|
||||
app:layout_constraintRight_toRightOf="@id/cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/cover" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
@ -86,6 +106,8 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
|
|
@ -13,23 +13,12 @@
|
|||
android:transitionGroup="true"
|
||||
tools:showIn="@layout/fragment_radios">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/native_art"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/alt_album_cover"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/cover"
|
||||
android:visibility="gone"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<com.github.apognu.otter.views.SquareImageView
|
||||
android:id="@+id/art"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/cover"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
|
|
Loading…
Reference in New Issue