mirror of
https://github.com/apognu/otter
synced 2025-02-25 22:47:37 +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.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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
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.name.text = playlist.name
|
||||||
holder.summary.text = context?.resources?.getQuantityString(R.plurals.playlist_description, playlist.tracks_count, playlist.tracks_count, toDurationString(playlist.duration.toLong())) ?: ""
|
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 ->
|
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
|
||||||
val imageView = when (index) {
|
val imageView = when (index) {
|
||||||
0 -> holder.cover_top_left
|
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_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
||||||
R.id.track_pin -> CommandBus.send(Command.PinTrack(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))
|
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
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
@ -11,7 +10,6 @@ import com.google.gson.Gson
|
|||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.apognu.otter.utils.Playlist
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.apognu.otter.utils.PlaylistsCache
|
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
||||||
import com.github.apognu.otter.utils.PlaylistsResponse
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
|
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
|
||||||
|
|
||||||
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||||
override val cacheId = "tracks-playlists"
|
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 val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||||
@ -16,3 +21,47 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
|
|||||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
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 Seek(val progress: Int) : Command()
|
||||||
|
|
||||||
class AddToQueue(val tracks: List<Track>) : Command()
|
class AddToQueue(val tracks: List<Track>) : Command()
|
||||||
|
class AddToPlaylist(val track: Track) : Command()
|
||||||
class PlayNext(val track: Track) : Command()
|
class PlayNext(val track: Track) : Command()
|
||||||
class ReplaceQueue(val queue: List<Track>, val fromRadio: Boolean = false) : Command()
|
class ReplaceQueue(val queue: List<Track>, val fromRadio: Boolean = false) : Command()
|
||||||
class RemoveFromQueue(val track: Track) : 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:id="@+id/track_play_next"
|
||||||
android:title="@string/playback_queue_play_next" />
|
android:title="@string/playback_queue_play_next" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/track_add_to_playlist"
|
||||||
|
android:title="Add to playlist" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/track_pin"
|
android:id="@+id/track_pin"
|
||||||
android:title="@string/playback_queue_download" />
|
android:title="@string/playback_queue_download" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user