diff --git a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt index 89d7813..0d30b86 100644 --- a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt +++ b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt @@ -379,6 +379,8 @@ class MainActivity : AppCompatActivity() { } is Command.RefreshTrack -> refreshCurrentTrack(command.track) + + is Command.AddToPlaylist -> AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.track) } } } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt index 1fb5d8d..d2a991b 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt @@ -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 diff --git a/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt index 4b0bf14..8d21397 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt @@ -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)) } diff --git a/app/src/main/java/com/github/apognu/otter/fragments/AddToPlaylistDialog.kt b/app/src/main/java/com/github/apognu/otter/fragments/AddToPlaylistDialog.kt new file mode 100644 index 0000000..f22a500 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/fragments/AddToPlaylistDialog.kt @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt index c72ab56..1de5b27 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt @@ -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 diff --git a/app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt index 9ecc506..492a71f 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt @@ -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, val allow_duplicates: Boolean) + class PlaylistsRepository(override val context: Context?) : Repository() { override val cacheId = "tracks-playlists" override val upstream = HttpUpstream>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken() {}.type) override fun cache(data: List) = PlaylistsCache(data) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader) +} + +class ManagementPlaylistsRepository(override val context: Context?) : Repository() { + override val cacheId = "tracks-playlists-management" + override val upstream = HttpUpstream>(HttpUpstream.Behavior.AtOnce, "/api/v1/playlists/?ordering=name", object : TypeToken() {}.type) + + override fun cache(data: List) = 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/utils/Bus.kt b/app/src/main/java/com/github/apognu/otter/utils/Bus.kt index 203e4f9..9b12d1a 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Bus.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Bus.kt @@ -22,6 +22,7 @@ sealed class Command { class Seek(val progress: Int) : Command() class AddToQueue(val tracks: List) : Command() + class AddToPlaylist(val track: Track) : Command() class PlayNext(val track: Track) : Command() class ReplaceQueue(val queue: List, val fromRadio: Boolean = false) : Command() class RemoveFromQueue(val track: Track) : Command() diff --git a/app/src/main/res/layout/dialog_add_to_playlist.xml b/app/src/main/res/layout/dialog_add_to_playlist.xml new file mode 100644 index 0000000..7ce7296 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_to_playlist.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/row_track.xml b/app/src/main/res/menu/row_track.xml index 4f1f0ac..86f9895 100644 --- a/app/src/main/res/menu/row_track.xml +++ b/app/src/main/res/menu/row_track.xml @@ -9,6 +9,10 @@ android:id="@+id/track_play_next" android:title="@string/playback_queue_play_next" /> + +