mirror of
https://github.com/apognu/otter
synced 2025-02-25 14:37:39 +01:00
#15: initial support for adding tracks to a playlist.
This commit is contained in:
parent
64ea222f08
commit
54d4dc2235
@ -379,6 +379,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
|
||||
is Command.AddToPlaylist -> AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
46
app/src/main/res/layout/dialog_add_to_playlist.xml
Normal file
46
app/src/main/res/layout/dialog_add_to_playlist.xml
Normal 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>
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user