Merge branch 'feature/129-favourite-sorting' into 'develop'

Sort Favourites by time

Closes #129

See merge request funkwhale/funkwhale-android!299
This commit is contained in:
Ryan Harg 2023-01-13 13:11:06 +00:00
commit ca63e0d60c
7 changed files with 70 additions and 47 deletions

View File

@ -8,11 +8,13 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowTrackBinding import audio.funkwhale.ffa.databinding.RowTrackBinding
import audio.funkwhale.ffa.fragments.FFAAdapter import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.model.Favorite
import audio.funkwhale.ffa.model.Track import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.utils.Command import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus import audio.funkwhale.ffa.utils.CommandBus
@ -27,7 +29,7 @@ class FavoritesAdapter(
private val context: Context?, private val context: Context?,
private val favoriteListener: FavoriteListener, private val favoriteListener: FavoriteListener,
val fromQueue: Boolean = false, val fromQueue: Boolean = false,
) : FFAAdapter<Track, FavoritesAdapter.ViewHolder>() { ) : FFAAdapter<Favorite, FavoritesAdapter.ViewHolder>() {
init { init {
this.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY this.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
@ -47,7 +49,7 @@ class FavoritesAdapter(
override fun applyFilter() { override fun applyFilter() {
data.clear() data.clear()
getUnfilteredData().map { getUnfilteredData().map {
if (it.matchesFilter(filter)) { if (it.track.matchesFilter(filter)) {
data.add(it) data.add(it)
} }
} }
@ -65,45 +67,43 @@ class FavoritesAdapter(
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val favorite = data[position] val favorite = data[position]
val track = favorite.track
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(favorite.cover())) CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(track.cover()))
.fit() .fit()
.placeholder(R.drawable.cover) .placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0)) .transform(RoundedCornersTransformation(16, 0))
.into(holder.cover) .into(holder.cover)
holder.title.text = favorite.title holder.title.text = track.title
holder.artist.text = favorite.artist.name holder.artist.text = track.artist.name
context?.let { context?.let {
holder.itemView.background = context.getDrawable(R.drawable.ripple) holder.itemView.background = AppCompatResources.getDrawable(context, R.drawable.ripple)
} }
if (favorite.id == currentTrack?.id) { if (track.id == currentTrack?.id) {
context?.let { context?.let {
holder.itemView.background = context.getDrawable(R.drawable.current) holder.itemView.background = AppCompatResources.getDrawable(context, R.drawable.current)
} }
} }
context?.let { context?.let {
when (favorite.favorite) { holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
}
when (favorite.cached || favorite.downloaded) { when (track.cached || track.downloaded) {
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0) true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
} }
if (favorite.cached && !favorite.downloaded) { if (track.cached && !track.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN) PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
} }
} }
if (favorite.downloaded) { if (track.downloaded) {
holder.title.compoundDrawables.forEach { holder.title.compoundDrawables.forEach {
it?.colorFilter = it?.colorFilter =
PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN) PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
@ -111,8 +111,7 @@ class FavoritesAdapter(
} }
holder.favorite.setOnClickListener { holder.favorite.setOnClickListener {
favoriteListener.onToggleFavorite(favorite.id, !favorite.favorite) favoriteListener.onToggleFavorite(track.id, !track.favorite)
data.remove(favorite) data.remove(favorite)
notifyItemRemoved(holder.bindingAdapterPosition) notifyItemRemoved(holder.bindingAdapterPosition)
} }
@ -125,10 +124,10 @@ class FavoritesAdapter(
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(favorite))) R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(favorite)) R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(favorite)) R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(favorite)) R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
} }
true true
@ -169,10 +168,13 @@ class FavoritesAdapter(
when (fromQueue) { when (fromQueue) {
true -> CommandBus.send(Command.PlayTrack(layoutPosition)) true -> CommandBus.send(Command.PlayTrack(layoutPosition))
false -> { false -> {
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply { data
CommandBus.send(Command.ReplaceQueue(this)) .subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition))
context.toast("All tracks were added to your queue") .map { it.track }
} .apply {
CommandBus.send(Command.ReplaceQueue(this))
context.toast("All tracks were added to your queue")
}
} }
} }
} }

View File

@ -46,7 +46,7 @@ abstract class FFAAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapte
abstract override fun getItemId(position: Int): Long abstract override fun getItemId(position: Int): Long
} }
abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() { abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>> : Fragment() {
companion object { companion object {
const val OFFSCREEN_PAGES = 20 const val OFFSCREEN_PAGES = 20
} }

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.adapters.FavoriteListener import audio.funkwhale.ffa.adapters.FavoriteListener
import audio.funkwhale.ffa.adapters.FavoritesAdapter import audio.funkwhale.ffa.adapters.FavoritesAdapter
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
import audio.funkwhale.ffa.model.Favorite
import audio.funkwhale.ffa.model.Track import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository import audio.funkwhale.ffa.repositories.TracksRepository
@ -31,7 +32,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() { class FavoritesFragment : FFAFragment<Favorite, FavoritesAdapter>() {
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java) private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
@ -63,7 +64,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
adapter.filter = s.toString() adapter.filter = s.toString()
@ -93,7 +94,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
} }
binding.play.setOnClickListener { binding.play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled())) CommandBus.send(Command.ReplaceQueue(adapter.data.map { it.track }.shuffled()))
} }
} }
@ -122,7 +123,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
withContext(Main) { withContext(Main) {
val data = adapter.data.map { val data = adapter.data.map {
it.downloaded = downloaded.contains(it.id) it.track.downloaded = downloaded.contains(it.id)
it it
}.toMutableList() }.toMutableList()
@ -138,7 +139,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id } adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }
.toList().getOrNull(0)?.let { match -> .toList().getOrNull(0)?.let { match ->
withContext(Main) { withContext(Main) {
adapter.data[match.second].downloaded = true adapter.data[match.second].track.downloaded = true
adapter.notifyItemChanged(match.second) adapter.notifyItemChanged(match.second)
} }
} }

View File

@ -5,6 +5,7 @@ sealed class CacheItem<D : Any>(val data: List<D>)
class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data) class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data)
class AlbumsCache(data: List<Album>) : CacheItem<Album>(data) class AlbumsCache(data: List<Album>) : CacheItem<Album>(data)
class TracksCache(data: List<Track>) : CacheItem<Track>(data) class TracksCache(data: List<Track>) : CacheItem<Track>(data)
class FavoritesCache(data: List<Favorite>) : CacheItem<Favorite>(data)
class PlaylistsCache(data: List<Playlist>) : CacheItem<Playlist>(data) class PlaylistsCache(data: List<Playlist>) : CacheItem<Playlist>(data)
class PlaylistTracksCache(data: List<PlaylistTrack>) : CacheItem<PlaylistTrack>(data) class PlaylistTracksCache(data: List<PlaylistTrack>) : CacheItem<PlaylistTrack>(data)
class RadiosCache(data: List<Radio>) : CacheItem<Radio>(data) class RadiosCache(data: List<Radio>) : CacheItem<Radio>(data)

View File

@ -0,0 +1,10 @@
package audio.funkwhale.ffa.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Favorite(
val id: Int = 0,
val track: Track
) : Parcelable

View File

@ -0,0 +1,9 @@
package audio.funkwhale.ffa.model
data class FavoritesResponse(
override val count: Int,
override val next: String?,
val results: List<Favorite>
) : FFAResponse<Favorite>() {
override fun getData() = results
}

View File

@ -2,11 +2,11 @@ package audio.funkwhale.ffa.repositories
import android.content.Context import android.content.Context
import audio.funkwhale.ffa.model.FFAResponse import audio.funkwhale.ffa.model.FFAResponse
import audio.funkwhale.ffa.model.Favorite
import audio.funkwhale.ffa.model.FavoritesResponse
import audio.funkwhale.ffa.model.FavoritedCache import audio.funkwhale.ffa.model.FavoritedCache
import audio.funkwhale.ffa.model.FavoritedResponse import audio.funkwhale.ffa.model.FavoritedResponse
import audio.funkwhale.ffa.model.Track import audio.funkwhale.ffa.model.FavoritesCache
import audio.funkwhale.ffa.model.TracksCache
import audio.funkwhale.ffa.model.TracksResponse
import audio.funkwhale.ffa.utils.FFACache import audio.funkwhale.ffa.utils.FFACache
import audio.funkwhale.ffa.utils.OAuth import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Settings import audio.funkwhale.ffa.utils.Settings
@ -28,7 +28,7 @@ import kotlinx.coroutines.runBlocking
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() { class FavoritesRepository(override val context: Context?) : Repository<Favorite, FavoritesCache>() {
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java) private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
private val exoCache: Cache by inject(Cache::class.java, named("exoCache")) private val exoCache: Cache by inject(Cache::class.java, named("exoCache"))
@ -36,34 +36,34 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
override val cacheId = "favorites.v2" override val cacheId = "favorites.v2"
override val upstream = HttpUpstream<Track, FFAResponse<Track>>( override val upstream = HttpUpstream<Favorite, FFAResponse<Favorite>>(
context!!, context!!,
HttpUpstream.Behavior.AtOnce, HttpUpstream.Behavior.AtOnce,
"/api/v1/tracks/?favorites=true&playable=true&ordering=title", "/api/v1/favorites/tracks/?scope=all&ordering=-creation_date",
object : TypeToken<TracksResponse>() {}.type, object : TypeToken<FavoritesResponse>() {}.type,
oAuth oAuth
) )
override fun cache(data: List<Track>) = TracksCache(data) override fun cache(data: List<Favorite>) = FavoritesCache(data)
override fun uncache(json: String) = override fun uncache(json: String) =
gsonDeserializerOf(TracksCache::class.java).deserialize(json.reader()) gsonDeserializerOf(FavoritesCache::class.java).deserialize(json.reader())
private val favoritedRepository = FavoritedRepository(context!!) private val favoritedRepository = FavoritedRepository(context!!)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking { override fun onDataFetched(data: List<Favorite>): List<Favorite> = runBlocking {
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf() val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
data.map { track -> data.map { favorite ->
track.favorite = true favorite.track.favorite = true
track.downloaded = downloaded.contains(track.id) favorite.track.downloaded = downloaded.contains(favorite.track.id)
track.bestUpload()?.let { upload -> favorite.track.bestUpload()?.let { upload ->
maybeNormalizeUrl(upload.listen_url)?.let { url -> maybeNormalizeUrl(upload.listen_url)?.let { url ->
track.cached = exoCache.isCached(url, 0, upload.duration * 1000L) favorite.track.cached = exoCache.isCached(url, 0, upload.duration * 1000L)
} }
} }
track favorite
} }
} }