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:
commit
ca63e0d60c
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue