#15: initial support for adding tracks to a playlist.

This commit is contained in:
Antoine POPINEAU 2020-09-04 13:26:08 +02:00
parent 64ea222f08
commit 54d4dc2235
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
9 changed files with 211 additions and 6 deletions

View File

@ -379,6 +379,8 @@ class MainActivity : AppCompatActivity() {
}
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
is Command.AddToPlaylist -> AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.track)
}
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.OtterAdapter
@ -36,6 +37,15 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
holder.name.text = playlist.name
holder.summary.text = context?.resources?.getQuantityString(R.plurals.playlist_description, playlist.tracks_count, playlist.tracks_count, toDurationString(playlist.duration.toLong())) ?: ""
context?.let {
ContextCompat.getDrawable(context, R.drawable.cover).let {
holder.cover_top_left.setImageDrawable(it)
holder.cover_top_right.setImageDrawable(it)
holder.cover_bottom_left.setImageDrawable(it)
holder.cover_bottom_right.setImageDrawable(it)
}
}
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
val imageView = when (index) {
0 -> holder.cover_top_left

View File

@ -115,6 +115,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.track_add_to_playlist -> CommandBus.send(Command.AddToPlaylist(track))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
}

View File

@ -0,0 +1,94 @@
package com.github.apognu.otter.fragments
import android.app.Activity
import android.app.AlertDialog
import android.view.View
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.PlaylistsAdapter
import com.github.apognu.otter.repositories.ManagementPlaylistsRepository
import com.github.apognu.otter.utils.*
import com.google.gson.Gson
import kotlinx.android.synthetic.main.dialog_add_to_playlist.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
object AddToPlaylistDialog {
fun show(activity: Activity, lifecycleScope: CoroutineScope, track: Track) {
val dialog = AlertDialog.Builder(activity).run {
setTitle("Add track to playlist")
setView(activity.layoutInflater.inflate(R.layout.dialog_add_to_playlist, null))
create()
}
dialog.show()
val repository = ManagementPlaylistsRepository(activity)
dialog.name.editText?.addTextChangedListener {
dialog.create.isEnabled = !(dialog.name.editText?.text?.trim()?.isBlank() ?: true)
}
dialog.create.setOnClickListener {
val name = dialog.name.editText?.text?.toString()?.trim() ?: ""
if (name.isEmpty()) return@setOnClickListener
lifecycleScope.launch(IO) {
repository.new(name)?.let { id ->
repository.add(id, track)
dialog.dismiss()
}
}
}
val adapter = PlaylistsAdapter(activity, object : PlaylistsAdapter.OnPlaylistClickListener {
override fun onClick(holder: View?, playlist: Playlist) {
repository.add(playlist.id, track)
dialog.dismiss()
}
})
dialog.playlists.layoutManager = LinearLayoutManager(activity)
dialog.playlists.adapter = adapter
repository.apply {
var first = true
fetch().untilNetwork(lifecycleScope) { data, isCache, _, hasMore ->
if (isCache) {
adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
return@untilNetwork
}
if (first) {
adapter.data.clear()
first = false
}
adapter.data.addAll(data)
lifecycleScope.launch(IO) {
try {
Cache.set(
context,
cacheId,
Gson().toJson(cache(adapter.data)).toByteArray()
)
} catch (e: ConcurrentModificationException) {
}
}
if (!hasMore) {
adapter.notifyDataSetChanged()
first = false
}
}
}
}
}

View File

@ -1,7 +1,6 @@
package com.github.apognu.otter.repositories
import android.content.Context
import androidx.lifecycle.lifecycleScope
import com.github.apognu.otter.Otter
import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.Fuel
@ -11,7 +10,6 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.BufferedReader

View File

@ -1,18 +1,67 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.OtterResponse
import com.github.apognu.otter.utils.Playlist
import com.github.apognu.otter.utils.PlaylistsCache
import com.github.apognu.otter.utils.PlaylistsResponse
import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedReader
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
override val cacheId = "tracks-playlists"
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
}
class ManagementPlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
override val cacheId = "tracks-playlists-management"
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.AtOnce, "/api/v1/playlists/?ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
suspend fun new(name: String): Int? {
val body = mapOf("name" to name, "privacy_level" to "me")
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
val (_, response, result) = request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitObjectResponseResult(gsonDeserializerOf(Playlist::class.java))
if (response.statusCode != 201) return null
return result.get().id
}
fun add(id: Int, track: Track) {
val body = PlaylistAdd(listOf(track.id), false)
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/add/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
scope.launch(Dispatchers.IO) {
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
}
}
}

View File

@ -22,6 +22,7 @@ sealed class Command {
class Seek(val progress: Int) : Command()
class AddToQueue(val tracks: List<Track>) : Command()
class AddToPlaylist(val track: Track) : Command()
class PlayNext(val track: Track) : Command()
class ReplaceQueue(val queue: List<Track>, val fromRadio: Boolean = false) : Command()
class RemoveFromQueue(val track: Track) : Command()

View File

@ -0,0 +1,46 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="16dp"
android:hint="New playlist..."
app:boxStrokeColor="@color/controlForeground"
app:hintTextColor="@color/controlForeground"
app:placeholderTextColor="@color/controlForeground">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/create"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="8dp"
android:enabled="false"
android:text="Create playlist"
android:textColor="@color/controlForeground"
app:rippleColor="@color/ripple" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/playlists"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:itemCount="10"
tools:listitem="@layout/row_playlist" />
</LinearLayout>

View File

@ -9,6 +9,10 @@
android:id="@+id/track_play_next"
android:title="@string/playback_queue_play_next" />
<item
android:id="@+id/track_add_to_playlist"
android:title="Add to playlist" />
<item
android:id="@+id/track_pin"
android:title="@string/playback_queue_download" />