mirror of
https://github.com/apognu/otter
synced 2025-02-17 11:20:34 +01:00
WIP - Integrate Room and LiveData.
This commit is contained in:
parent
e60814d28f
commit
567a7476f9
@ -6,10 +6,14 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-android-extensions")
|
||||
id("kotlin-kapt")
|
||||
id("realm-android")
|
||||
|
||||
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
|
||||
id("com.gladed.androidgitversion") version "0.4.10"
|
||||
id("com.github.triplet.play") version "2.4.2"
|
||||
|
||||
kotlin("plugin.serialization") version "1.3.70"
|
||||
}
|
||||
|
||||
val props = Properties().apply {
|
||||
@ -63,9 +67,9 @@ android {
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".dev"
|
||||
applicationIdSuffix = ".dev.livedata"
|
||||
manifestPlaceholders = mapOf(
|
||||
"app_name" to "Otter (develop)"
|
||||
"app_name" to "Otter (livedata)"
|
||||
)
|
||||
|
||||
resValue("string", "debug.hostname", props.getProperty("debug.hostname", ""))
|
||||
@ -118,10 +122,15 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0")
|
||||
|
||||
implementation("androidx.appcompat:appcompat:1.2.0")
|
||||
implementation("androidx.core:core-ktx:1.5.0-alpha02")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
|
||||
implementation("androidx.fragment:fragment-ktx:1.2.5")
|
||||
implementation("androidx.room:room-runtime:2.2.5")
|
||||
implementation("androidx.room:room-ktx:2.2.5")
|
||||
implementation("androidx.paging:paging-runtime:3.0.0-alpha06")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.preference:preference:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||
@ -134,11 +143,21 @@ dependencies {
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
|
||||
|
||||
implementation("com.aliassadi:power-preference-lib:1.4.1")
|
||||
implementation("com.github.kittinunf.fuel:fuel:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel:2.2.3")
|
||||
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("com.squareup.picasso:picasso:2.71828")
|
||||
implementation("jp.wasabeef:picasso-transformations:2.2.1")
|
||||
|
||||
debugImplementation("com.amitshekhar.android:debug-db:1.0.6")
|
||||
|
||||
kapt("androidx.room:room-compiler:2.2.5")
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {
|
||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlinx.serialization.ImplicitReflectionSerializer"
|
||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlinx.serialization.UnstableDefault"
|
||||
}
|
||||
|
@ -2,9 +2,14 @@ package com.github.apognu.otter
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.room.Room
|
||||
import com.github.apognu.otter.models.dao.OtterDatabase
|
||||
import com.github.apognu.otter.playback.MediaSession
|
||||
import com.github.apognu.otter.playback.QueueManager
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
||||
@ -14,8 +19,8 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import com.preference.PowerPreference
|
||||
import io.realm.Realm
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -30,8 +35,12 @@ class Otter : Application() {
|
||||
|
||||
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
|
||||
val commandBus: BroadcastChannel<Command> = BroadcastChannel(10)
|
||||
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
|
||||
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
||||
|
||||
val database: OtterDatabase by lazy {
|
||||
Room
|
||||
.databaseBuilder(this, OtterDatabase::class.java, "otter")
|
||||
.build()
|
||||
}
|
||||
|
||||
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
|
||||
|
||||
@ -66,6 +75,8 @@ class Otter : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Realm.init(this)
|
||||
|
||||
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
||||
|
@ -3,13 +3,16 @@ package com.github.apognu.otter.activities
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.DownloadsAdapter
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.getMetadata
|
||||
import com.github.apognu.otter.viewmodels.DownloadsViewModel
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import kotlinx.android.synthetic.main.activity_downloads.*
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
@ -27,15 +30,20 @@ class DownloadsActivity : AppCompatActivity() {
|
||||
|
||||
setContentView(R.layout.activity_downloads)
|
||||
|
||||
downloads.itemAnimator = null
|
||||
(downloads.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
|
||||
adapter = DownloadsAdapter(this, DownloadChangedListener()).also {
|
||||
adapter = DownloadsAdapter(this).also {
|
||||
it.setHasStableIds(true)
|
||||
|
||||
downloads.layoutManager = LinearLayoutManager(this)
|
||||
downloads.adapter = it
|
||||
}
|
||||
|
||||
DownloadsViewModel.get().downloads.observe(this) { downloads ->
|
||||
adapter.downloads = downloads.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Default) {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
@ -54,40 +62,20 @@ class DownloadsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
lifecycleScope.launch(Main) {
|
||||
val cursor = Otter.get().exoDownloadManager.downloadIndex.getDownloads()
|
||||
|
||||
adapter.downloads.clear()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val download = cursor.download
|
||||
|
||||
download.getMetadata()?.let { info ->
|
||||
adapter.downloads.add(info.apply {
|
||||
this.download = download
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshTrack(download: Download) {
|
||||
download.getMetadata()?.let { info ->
|
||||
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||
if (download.state != info.download?.state) {
|
||||
withContext(Main) {
|
||||
adapter.downloads[match.second] = info.apply {
|
||||
this.download = download
|
||||
}
|
||||
adapter.downloads.getOrNull(match.second)?.let {
|
||||
withContext(Main) {
|
||||
adapter.downloads[match.second] = info.apply {
|
||||
this.download = download
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(match.second)
|
||||
adapter.notifyItemChanged(match.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,11 +103,4 @@ class DownloadsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class DownloadChangedListener : DownloadsAdapter.OnDownloadChangedListener {
|
||||
override fun onItemRemoved(index: Int) {
|
||||
adapter.downloads.removeAt(index)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,20 +11,21 @@ import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.LoginDialog
|
||||
import com.github.apognu.otter.models.api.Credentials
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Userinfo
|
||||
import com.github.apognu.otter.utils.log
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.kittinunf.result.Result
|
||||
import com.google.gson.Gson
|
||||
import com.preference.PowerPreference
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
|
||||
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
||||
|
||||
@ImplicitReflectionSerializer
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -102,11 +103,13 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
try {
|
||||
val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body)
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
val (_, response, result) =
|
||||
Fuel
|
||||
.post("$hostname/api/v1/token/", body)
|
||||
.awaitObjectResponseResult<Credentials>(AppContext.deserializer())
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
is Result.Success<*> -> {
|
||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||
setString("hostname", hostname)
|
||||
setBoolean("anonymous", false)
|
||||
@ -128,7 +131,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
is Result.Failure -> {
|
||||
dialog.dismiss()
|
||||
|
||||
val error = Gson().fromJson(String(response.data), FwCredentials::class.java)
|
||||
val error = Gson().fromJson(String(response.data), Credentials::class.java)
|
||||
|
||||
hostname_field.error = null
|
||||
username_field.error = null
|
||||
@ -160,7 +163,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
lifecycleScope.launch(Main) {
|
||||
try {
|
||||
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
.awaitObjectResponseResult<Credentials>(AppContext.deserializer())
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
|
@ -2,7 +2,6 @@ package com.github.apognu.otter.activities
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
@ -10,7 +9,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.*
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
@ -21,15 +19,19 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.apognu.otter.Otter
|
||||
import androidx.lifecycle.observe
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.*
|
||||
import com.github.apognu.otter.models.dao.RealmArtist
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.playback.MediaControlsManager
|
||||
import com.github.apognu.otter.playback.PinService
|
||||
import com.github.apognu.otter.playback.PlayerService
|
||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||
import com.github.apognu.otter.viewmodels.QueueViewModel
|
||||
import com.github.apognu.otter.views.DisableableFrameLayout
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
|
||||
@ -38,6 +40,7 @@ import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.gson.Gson
|
||||
import com.preference.PowerPreference
|
||||
import com.squareup.picasso.Picasso
|
||||
import io.realm.Realm
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.partial_now_playing.*
|
||||
@ -53,7 +56,8 @@ class MainActivity : AppCompatActivity() {
|
||||
LOGOUT(1001)
|
||||
}
|
||||
|
||||
private val favoriteRepository = FavoritesRepository(this)
|
||||
private val queueViewModel = QueueViewModel.get()
|
||||
private val favoritesRepository = FavoritesRepository(this)
|
||||
private val favoritedRepository = FavoritedRepository(this)
|
||||
private var menu: Menu? = null
|
||||
|
||||
@ -75,12 +79,51 @@ class MainActivity : AppCompatActivity() {
|
||||
.commit()
|
||||
|
||||
watchEventBus()
|
||||
|
||||
PlayerStateViewModel.get().isPlaying.observe(this) { isPlaying ->
|
||||
when (isPlaying) {
|
||||
true -> {
|
||||
now_playing_toggle.icon = getDrawable(R.drawable.pause)
|
||||
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
|
||||
}
|
||||
|
||||
false -> {
|
||||
now_playing_toggle.icon = getDrawable(R.drawable.play)
|
||||
now_playing_details_toggle.icon = getDrawable(R.drawable.play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlayerStateViewModel.get().isBuffering.observe(this) { isBuffering ->
|
||||
when (isBuffering) {
|
||||
true -> now_playing_buffering.visibility = View.VISIBLE
|
||||
false -> now_playing_buffering.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
PlayerStateViewModel.get().track.observe(this) { track ->
|
||||
refreshCurrentTrack(track)
|
||||
}
|
||||
|
||||
PlayerStateViewModel.get().position.observe(this) { (current, duration, percent) ->
|
||||
now_playing_progress.progress = percent
|
||||
now_playing_details_progress.progress = percent
|
||||
|
||||
val currentMins = (current / 1000) / 60
|
||||
val currentSecs = (current / 1000) % 60
|
||||
|
||||
val durationMins = duration / 60
|
||||
val durationSecs = duration % 60
|
||||
|
||||
now_playing_details_progress_current.text = "%02d:%02d".format(currentMins, currentSecs)
|
||||
now_playing_details_progress_duration.text = "%02d:%02d".format(durationMins, durationSecs)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
|
||||
container?.setShouldRegisterTouch { _ ->
|
||||
if (now_playing.isOpened()) {
|
||||
now_playing.close()
|
||||
|
||||
@ -90,7 +133,17 @@ class MainActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
favoritedRepository.update(this, lifecycleScope)
|
||||
landscape_queue?.setShouldRegisterTouch { _ ->
|
||||
if (now_playing.isOpened()) {
|
||||
now_playing.close()
|
||||
|
||||
return@setShouldRegisterTouch false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
favoritedRepository.update()
|
||||
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
DownloadService.start(this, PinService::class.java)
|
||||
@ -257,6 +310,11 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
||||
startActivity(this)
|
||||
|
||||
databaseList().forEach {
|
||||
deleteDatabase(it)
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -300,13 +358,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
is Event.PlaybackError -> toast(message.message)
|
||||
|
||||
is Event.Buffering -> {
|
||||
when (message.value) {
|
||||
true -> now_playing_buffering.visibility = View.VISIBLE
|
||||
false -> now_playing_buffering.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
is Event.PlaybackStopped -> {
|
||||
if (now_playing.visibility == View.VISIBLE) {
|
||||
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
@ -332,30 +383,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
is Event.TrackFinished -> incrementListenCount(message.track)
|
||||
|
||||
is Event.StateChanged -> {
|
||||
when (message.playing) {
|
||||
true -> {
|
||||
now_playing_toggle.icon = getDrawable(R.drawable.pause)
|
||||
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
|
||||
}
|
||||
|
||||
false -> {
|
||||
now_playing_toggle.icon = getDrawable(R.drawable.play)
|
||||
now_playing_details_toggle.icon = getDrawable(R.drawable.play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Event.QueueChanged -> {
|
||||
findViewById<View>(R.id.nav_queue)?.let { view ->
|
||||
ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let {
|
||||
it.duration = 500
|
||||
it.interpolator = AccelerateDecelerateInterpolator()
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -377,27 +404,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
ProgressBus.get().collect { (current, duration, percent) ->
|
||||
now_playing_progress.progress = percent
|
||||
now_playing_details_progress.progress = percent
|
||||
|
||||
val currentMins = (current / 1000) / 60
|
||||
val currentSecs = (current / 1000) % 60
|
||||
|
||||
val durationMins = duration / 60
|
||||
val durationSecs = duration % 60
|
||||
|
||||
now_playing_details_progress_current.text = "%02d:%02d".format(currentMins, currentSecs)
|
||||
now_playing_details_progress_duration.text = "%02d:%02d".format(durationMins, durationSecs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
@ -424,15 +433,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
now_playing_title.text = track.title
|
||||
now_playing_album.text = track.artist.name
|
||||
now_playing_album.text = track.artist?.name
|
||||
now_playing_toggle.icon = getDrawable(R.drawable.pause)
|
||||
|
||||
now_playing_details_title.text = track.title
|
||||
now_playing_details_artist.text = track.artist.name
|
||||
now_playing_details_artist.text = track.artist?.name
|
||||
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.into(now_playing_cover)
|
||||
@ -499,34 +508,22 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
now_playing_details_favorite?.let { now_playing_details_favorite ->
|
||||
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
|
||||
lifecycleScope.launch(Main) {
|
||||
track.favorite = favorites.contains(track.id)
|
||||
when (track.favorite) {
|
||||
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
|
||||
when (track.favorite) {
|
||||
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now_playing_details_favorite.setOnClickListener {
|
||||
when (track.favorite) {
|
||||
true -> {
|
||||
favoriteRepository.deleteFavorite(track.id)
|
||||
now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
|
||||
false -> {
|
||||
favoriteRepository.addFavorite(track.id)
|
||||
now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
}
|
||||
now_playing_details_favorite.setOnClickListener {
|
||||
when (track.favorite) {
|
||||
true -> {
|
||||
favoritesRepository.deleteFavorite(track.id)
|
||||
// now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
|
||||
track.favorite = !track.favorite
|
||||
|
||||
favoriteRepository.fetch(Repository.Origin.Network.origin)
|
||||
false -> {
|
||||
favoritesRepository.addFavorite(track.id)
|
||||
// now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,24 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.SearchAdapter
|
||||
import com.github.apognu.otter.fragments.AlbumsFragment
|
||||
import com.github.apognu.otter.fragments.ArtistsFragment
|
||||
import com.github.apognu.otter.models.dao.toDao
|
||||
import com.github.apognu.otter.repositories.*
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import kotlinx.android.synthetic.main.activity_search.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
@ -47,7 +53,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(IO) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||
@ -82,25 +88,52 @@ class SearchActivity : AppCompatActivity() {
|
||||
adapter.tracks.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
|
||||
artistsRepository.fetch().untilNetwork(lifecycleScope, IO) { artists, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.artists.addAll(artists)
|
||||
refresh()
|
||||
artists.forEach {
|
||||
Otter.get().database.artists().run {
|
||||
insert(it.toDao())
|
||||
|
||||
adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _, _ ->
|
||||
albumsRepository.fetch().untilNetwork(lifecycleScope, IO) { albums, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.albums.addAll(albums)
|
||||
refresh()
|
||||
albums.forEach {
|
||||
Otter.get().database.albums().run {
|
||||
insert(it.toDao())
|
||||
|
||||
adapter.albums.add(Album.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ ->
|
||||
tracksRepository.fetch().untilNetwork(lifecycleScope, IO) { tracks, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.tracks.addAll(tracks)
|
||||
refresh()
|
||||
tracks.forEach {
|
||||
Otter.get().database.tracks().run {
|
||||
insertWithAssocs(it)
|
||||
|
||||
adapter.tracks.add(Track.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,14 +160,14 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
download.getMetadata()?.let { info ->
|
||||
/* download.getMetadata()?.let { info ->
|
||||
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.tracks[match.second].downloaded = true
|
||||
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -35,6 +36,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
fun getThemeResId(): Int = R.style.AppTheme
|
||||
}
|
||||
|
||||
@ImplicitReflectionSerializer
|
||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
@ -7,9 +7,9 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_album.view.*
|
||||
@ -42,7 +42,7 @@ class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickLis
|
||||
.into(holder.art)
|
||||
|
||||
holder.title.text = album.title
|
||||
holder.artist.text = album.artist.name
|
||||
holder.artist.text = album.artist_name
|
||||
holder.release_date.visibility = View.GONE
|
||||
|
||||
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
|
||||
|
@ -7,9 +7,9 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_album_grid.view.*
|
||||
|
@ -7,39 +7,21 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_artist.view.*
|
||||
|
||||
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
||||
private var active: List<Artist> = mutableListOf()
|
||||
|
||||
interface OnArtistClickListener {
|
||||
fun onClick(holder: View?, artist: Artist)
|
||||
}
|
||||
|
||||
init {
|
||||
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
active = data.filter { it.albums?.isNotEmpty() ?: false }
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
super.onChanged()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
active = data.filter { it.albums?.isNotEmpty() ?: false }
|
||||
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getItemCount() = active.size
|
||||
|
||||
override fun getItemId(position: Int) = active[position].id.toLong()
|
||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
|
||||
@ -50,24 +32,18 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val artist = active[position]
|
||||
val artist = data[position]
|
||||
|
||||
artist.albums?.let { albums ->
|
||||
if (albums.isNotEmpty()) {
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(albums[0].cover?.urls?.original))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
}
|
||||
}
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(artist.album_cover))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
|
||||
holder.name.text = artist.name
|
||||
|
||||
artist.albums?.let {
|
||||
context?.let {
|
||||
holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.albums.size, artist.albums.size)
|
||||
}
|
||||
context?.let {
|
||||
holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.album_count, artist.album_count)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +53,9 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
||||
val albums = view.albums
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(view, active[layoutPosition])
|
||||
data[layoutPosition].let { artist ->
|
||||
listener.onClick(view, artist)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,23 +7,20 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.api.DownloadInfo
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.playback.PinService
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import kotlinx.android.synthetic.main.row_download.view.*
|
||||
|
||||
class DownloadsAdapter(private val context: Context, private val listener: OnDownloadChangedListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
|
||||
interface OnDownloadChangedListener {
|
||||
fun onItemRemoved(index: Int)
|
||||
}
|
||||
|
||||
class DownloadsAdapter(private val context: Context) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
|
||||
var downloads: MutableList<DownloadInfo> = mutableListOf()
|
||||
|
||||
override fun getItemCount() = downloads.size
|
||||
|
||||
override fun getItemId(position: Int) = downloads[position].id.toLong()
|
||||
|
||||
override fun getItemCount() = downloads.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_download, parent, false)
|
||||
|
||||
@ -79,8 +76,8 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
|
||||
Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false)
|
||||
|
||||
Download.STATE_FAILED -> {
|
||||
Track.fromDownload(download).also {
|
||||
PinService.download(context, it)
|
||||
FunkwhaleTrack.fromDownload(download).also {
|
||||
// PinService.download(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +86,6 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
|
||||
}
|
||||
|
||||
holder.delete.setOnClickListener {
|
||||
listener.onItemRemoved(position)
|
||||
DownloadService.sendRemoveDownload(context, PinService::class.java, download.contentId, false)
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
@ -51,7 +52,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
|
||||
.into(holder.cover)
|
||||
|
||||
holder.title.text = favorite.title
|
||||
holder.artist.text = favorite.artist.name
|
||||
holder.artist.text = favorite.artist?.name
|
||||
|
||||
context?.let {
|
||||
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
||||
|
@ -13,12 +13,13 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
import java.util.*
|
||||
|
||||
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : OtterAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
|
||||
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : OtterAdapter<Track, PlaylistTracksAdapter.ViewHolder>() {
|
||||
interface OnFavoriteListener {
|
||||
fun onToggleFavorite(id: Int, state: Boolean)
|
||||
}
|
||||
@ -30,7 +31,7 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return data[position].track.id.toLong()
|
||||
return data[position].id.toLong()
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
@ -56,38 +57,33 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||
val track = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.track.album?.cover()))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(holder.cover)
|
||||
|
||||
holder.title.text = track.track.title
|
||||
holder.artist.text = track.track.artist.name
|
||||
holder.title.text = track.title
|
||||
holder.artist.text = track.artist?.name
|
||||
|
||||
context?.let {
|
||||
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
||||
}
|
||||
|
||||
if (track.track == currentTrack || track.track.current) {
|
||||
if (track == currentTrack) {
|
||||
context?.let {
|
||||
holder.itemView.background = context.getDrawable(R.drawable.current)
|
||||
}
|
||||
}
|
||||
|
||||
context?.let {
|
||||
when (track.track.favorite) {
|
||||
when (track.favorite) {
|
||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
||||
}
|
||||
|
||||
holder.favorite.setOnClickListener {
|
||||
favoriteListener?.let {
|
||||
favoriteListener.onToggleFavorite(track.track.id, !track.track.favorite)
|
||||
|
||||
track.track.favorite = !track.track.favorite
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,9 +94,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track)))
|
||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track))
|
||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.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.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
||||
}
|
||||
|
||||
true
|
||||
@ -152,8 +148,8 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||
when (fromQueue) {
|
||||
true -> CommandBus.send(Command.PlayTrack(layoutPosition))
|
||||
false -> {
|
||||
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
|
||||
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
|
||||
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).also { track ->
|
||||
CommandBus.send(Command.ReplaceQueue(track))
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
|
@ -6,16 +6,16 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Playlist
|
||||
import com.github.apognu.otter.utils.toDurationString
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_playlist.view.*
|
||||
|
||||
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : OtterAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
|
||||
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : OtterAdapter<PlaylistEntity, PlaylistsAdapter.ViewHolder>() {
|
||||
interface OnPlaylistClickListener {
|
||||
fun onClick(holder: View?, playlist: Playlist)
|
||||
fun onClick(holder: View?, playlist: PlaylistEntity)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
@ -6,11 +6,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.views.LoadingImageView
|
||||
import com.preference.PowerPreference
|
||||
import kotlinx.android.synthetic.main.row_radio.view.*
|
||||
@ -20,9 +20,9 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : OtterAdapter<Radio, RadiosAdapter.ViewHolder>() {
|
||||
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : OtterAdapter<RadioEntity, RadiosAdapter.ViewHolder>() {
|
||||
interface OnRadioClickListener {
|
||||
fun onClick(holder: ViewHolder, radio: Radio)
|
||||
fun onClick(holder: ViewHolder, radio: RadioEntity)
|
||||
}
|
||||
|
||||
enum class RowType {
|
||||
@ -31,26 +31,26 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
|
||||
UserRadio
|
||||
}
|
||||
|
||||
private val instanceRadios: List<Radio> by lazy {
|
||||
private val instanceRadios: List<RadioEntity> by lazy {
|
||||
context?.let {
|
||||
return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) {
|
||||
"" -> listOf(
|
||||
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description))
|
||||
RadioEntity(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description))
|
||||
)
|
||||
|
||||
else -> listOf(
|
||||
Radio(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username),
|
||||
Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)),
|
||||
Radio(0, "favorites", context.getString(R.string.favorites), context.getString(R.string.radio_favorites_description)),
|
||||
Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description))
|
||||
RadioEntity(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username),
|
||||
RadioEntity(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)),
|
||||
RadioEntity(0, "favorites", context.getString(R.string.favorites), context.getString(R.string.radio_favorites_description)),
|
||||
RadioEntity(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
listOf<Radio>()
|
||||
listOf<RadioEntity>()
|
||||
}
|
||||
|
||||
private fun getRadioAt(position: Int): Radio {
|
||||
private fun getRadioAt(position: Int): RadioEntity {
|
||||
return when (getItemViewType(position)) {
|
||||
RowType.InstanceRadio.ordinal -> instanceRadios[position - 1]
|
||||
else -> data[position - instanceRadios.size - 2]
|
||||
|
@ -13,7 +13,11 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
@ -41,7 +45,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||
var albums: MutableList<Album> = mutableListOf()
|
||||
var tracks: MutableList<Track> = mutableListOf()
|
||||
|
||||
var currentTrack: Track? = null
|
||||
var currentTrack: FunkwhaleTrack? = null
|
||||
|
||||
override fun getItemCount() = SECTION_COUNT + artists.size + albums.size + tracks.size
|
||||
|
||||
@ -169,10 +173,10 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||
if (resultType == ResultType.Track.ordinal) {
|
||||
(item as? Track)?.let { track ->
|
||||
context?.let { context ->
|
||||
if (track == currentTrack || track.current) {
|
||||
/* if (track == currentTrack || track.current) {
|
||||
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
|
||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
|
||||
}
|
||||
} */
|
||||
|
||||
when (track.favorite) {
|
||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||
@ -180,13 +184,7 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||
}
|
||||
|
||||
holder.favorite.setOnClickListener {
|
||||
favoriteListener?.let {
|
||||
favoriteListener.onToggleFavorite(track.id, !track.favorite)
|
||||
|
||||
tracks[position - artists.size - albums.size - SECTION_COUNT].favorite = !track.favorite
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||
}
|
||||
|
||||
when (track.cached || track.downloaded) {
|
||||
|
@ -2,7 +2,9 @@ package com.github.apognu.otter.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
@ -11,6 +13,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
@ -24,8 +27,6 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||
|
||||
private lateinit var touchHelper: ItemTouchHelper
|
||||
|
||||
var currentTrack: Track? = null
|
||||
|
||||
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
@ -59,32 +60,22 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||
.into(holder.cover)
|
||||
|
||||
holder.title.text = track.title
|
||||
holder.artist.text = track.artist.name
|
||||
holder.artist.text = track.artist?.name
|
||||
|
||||
context?.let {
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
|
||||
}
|
||||
|
||||
if (track == currentTrack || track.current) {
|
||||
context?.let {
|
||||
if (track.current) {
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
|
||||
}
|
||||
}
|
||||
|
||||
context?.let {
|
||||
when (track.favorite) {
|
||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
||||
}
|
||||
|
||||
holder.favorite.setOnClickListener {
|
||||
favoriteListener?.let {
|
||||
favoriteListener.onToggleFavorite(track.id, !track.favorite)
|
||||
|
||||
data[position].favorite = !track.favorite
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||
}
|
||||
|
||||
when (track.cached || track.downloaded) {
|
||||
|
@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
@ -16,21 +17,21 @@ import androidx.transition.Slide
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.adapters.AlbumsAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||
import com.github.apognu.otter.repositories.AlbumsRepository
|
||||
import com.github.apognu.otter.repositories.ArtistTracksRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.viewmodels.AlbumsViewModel
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.fragment_albums.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||
class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
|
||||
override lateinit var liveData: LiveData<List<Album>>
|
||||
override val viewRes = R.layout.fragment_albums
|
||||
override val recycler: RecyclerView get() = albums
|
||||
override val alwaysRefresh = false
|
||||
@ -92,14 +93,16 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.apply {
|
||||
artistId = getInt("artistId")
|
||||
artistName = getString("artistName") ?: ""
|
||||
artistArt = getString("artistArt") ?: ""
|
||||
}
|
||||
|
||||
liveData = AlbumsViewModel(artistId).albums
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = AlbumsAdapter(context, OnAlbumClickListener())
|
||||
repository = AlbumsRepository(context, artistId)
|
||||
artistTracksRepository = ArtistTracksRepository(context, artistId)
|
||||
@ -132,19 +135,18 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||
play.isClickable = false
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
artistTracksRepository.fetch(Repository.Origin.Network.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
.shuffled()
|
||||
.also {
|
||||
CommandBus.send(Command.ReplaceQueue(it))
|
||||
artistTracksRepository.fetch().untilNetwork(lifecycleScope) { _, _, _ ->
|
||||
loader.stop()
|
||||
|
||||
withContext(Main) {
|
||||
play.icon = requireContext().getDrawable(R.drawable.play)
|
||||
play.isClickable = true
|
||||
play.icon = requireContext().getDrawable(R.drawable.play)
|
||||
play.isClickable = true
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
AlbumsViewModel(artistId).tracks().also {
|
||||
CommandBus.send(Command.ReplaceQueue(it.shuffled()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,15 @@ import androidx.transition.Slide
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.adapters.AlbumsGridAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||
import com.github.apognu.otter.repositories.AlbumsRepository
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.viewmodels.AlbumsViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_albums_grid.*
|
||||
|
||||
class AlbumsGridFragment : OtterFragment<Album, AlbumsGridAdapter>() {
|
||||
class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
|
||||
override val liveData = AlbumsViewModel().albums
|
||||
override val viewRes = R.layout.fragment_albums_grid
|
||||
override val recycler: RecyclerView get() = albums
|
||||
override val layoutManager get() = GridLayoutManager(context, 3)
|
||||
|
@ -2,57 +2,66 @@ package com.github.apognu.otter.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.Slide
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.adapters.ArtistsAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||
import com.github.apognu.otter.repositories.ArtistsRepository
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.onViewPager
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
import com.github.apognu.otter.viewmodels.ArtistsViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||
|
||||
class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
|
||||
class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
|
||||
override val liveData = ArtistsViewModel.get().artists
|
||||
override val viewRes = R.layout.fragment_artists
|
||||
override val recycler: RecyclerView get() = artists
|
||||
override val alwaysRefresh = false
|
||||
|
||||
companion object {
|
||||
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {
|
||||
(context as? MainActivity)?.let {
|
||||
fragment?.let { fragment ->
|
||||
fragment.onViewPager {
|
||||
exitTransition = Fade().apply {
|
||||
duration = AppContext.TRANSITION_DURATION
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
fun openAlbums(context: Context?, artist: Artist?, fragment: Fragment? = null, art: String? = null) {
|
||||
artist?.let {
|
||||
(context as? MainActivity)?.let {
|
||||
fragment?.let { fragment ->
|
||||
fragment.onViewPager {
|
||||
exitTransition = Fade().apply {
|
||||
duration = AppContext.TRANSITION_DURATION
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
view?.let {
|
||||
addTarget(it)
|
||||
view?.let {
|
||||
addTarget(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(context as? AppCompatActivity)?.let { activity ->
|
||||
val nextFragment = AlbumsFragment.new(artist, art).apply {
|
||||
enterTransition = Slide().apply {
|
||||
duration = AppContext.TRANSITION_DURATION
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
(context as? AppCompatActivity)?.let { activity ->
|
||||
val nextFragment = AlbumsFragment.new(artist, art).apply {
|
||||
enterTransition = Slide().apply {
|
||||
duration = AppContext.TRANSITION_DURATION
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity.supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.container, nextFragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
activity.supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.container, nextFragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,9 +73,28 @@ class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
|
||||
repository = ArtistsRepository(context)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_artists, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
artists.layoutManager = LinearLayoutManager(context)
|
||||
(artists.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
artists.adapter = adapter
|
||||
|
||||
liveData.observe(viewLifecycleOwner) { result ->
|
||||
adapter.data.size.let { position ->
|
||||
adapter.data = result.toMutableList()
|
||||
adapter.notifyItemInserted(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
|
||||
override fun onClick(holder: View?, artist: Artist) {
|
||||
openAlbums(context, artist, fragment = this@ArtistsFragment)
|
||||
openAlbums(context, artist, this@ArtistsFragment, artist.album_cover)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,26 @@ package com.github.apognu.otter.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.FavoritesAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.TracksRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||
import com.github.apognu.otter.viewmodels.TracksViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_favorites.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
class FavoritesFragment : LiveOtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
|
||||
override val liveData = TracksViewModel(0).favorites
|
||||
override val viewRes = R.layout.fragment_favorites
|
||||
override val recycler: RecyclerView get() = favorites
|
||||
override val alwaysRefresh = false
|
||||
@ -27,23 +32,14 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
adapter = FavoritesAdapter(context, FavoriteListener())
|
||||
repository = FavoritesRepository(context)
|
||||
|
||||
PlayerStateViewModel.get().track.observe(this) { refreshCurrentTrack(it) }
|
||||
|
||||
watchEventBus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response ->
|
||||
withContext(Main) {
|
||||
adapter.currentTrack = response.track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
refreshDownloadedTracks()
|
||||
}
|
||||
|
||||
play.setOnClickListener {
|
||||
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||
}
|
||||
@ -53,15 +49,7 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
lifecycleScope.launch(Main) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
// is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,17 +58,17 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
private suspend fun refreshDownloadedTracks() {
|
||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
|
||||
withContext(Main) {
|
||||
/* withContext(Main) {
|
||||
adapter.data = adapter.data.map {
|
||||
it.downloaded = downloaded.contains(it.id)
|
||||
it
|
||||
}.toMutableList()
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
||||
/* private suspend fun refreshDownloadedTrack(download: Download) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
download.getMetadata()?.let { info ->
|
||||
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||
@ -91,14 +79,11 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
track?.let {
|
||||
adapter.currentTrack?.current = false
|
||||
adapter.currentTrack = track.apply {
|
||||
current = true
|
||||
}
|
||||
adapter.currentTrack = track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
@ -5,29 +5,35 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.TracksAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.viewmodels.QueueViewModel
|
||||
import kotlinx.android.synthetic.main.partial_queue.*
|
||||
import kotlinx.android.synthetic.main.partial_queue.view.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LandscapeQueueFragment : Fragment() {
|
||||
private var adapter: TracksAdapter? = null
|
||||
|
||||
private val viewModel = QueueViewModel.get()
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
watchEventBus()
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
viewModel.queue.observe(viewLifecycleOwner) {
|
||||
refresh(it)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.partial_queue, container, false).apply {
|
||||
adapter = TracksAdapter(context, fromQueue = true).also {
|
||||
adapter = TracksAdapter(context, FavoriteListener(), fromQueue = true).also {
|
||||
queue.layoutManager = LinearLayoutManager(context)
|
||||
queue.adapter = it
|
||||
}
|
||||
@ -39,43 +45,28 @@ class LandscapeQueueFragment : Fragment() {
|
||||
|
||||
queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
activity?.lifecycleScope?.launch(Main) {
|
||||
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
|
||||
adapter?.let {
|
||||
it.data = response.queue.toMutableList()
|
||||
it.notifyDataSetChanged()
|
||||
private fun refresh(tracks: List<Track>) {
|
||||
adapter?.let {
|
||||
it.data = tracks.toMutableList()
|
||||
it.notifyDataSetChanged()
|
||||
|
||||
if (it.data.isEmpty()) {
|
||||
queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
} else {
|
||||
queue?.visibility = View.VISIBLE
|
||||
placeholder?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
if (it.data.isEmpty()) {
|
||||
queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
} else {
|
||||
queue?.visibility = View.VISIBLE
|
||||
placeholder?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchEventBus() {
|
||||
activity?.lifecycleScope?.launch(Main) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.QueueChanged -> refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity?.lifecycleScope?.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshTrack -> refresh()
|
||||
}
|
||||
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||
when (state) {
|
||||
true -> favoritesRepository.addFavorite(id)
|
||||
false -> favoritesRepository.deleteFavorite(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,17 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.github.apognu.otter.repositories.HttpUpstream
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.gson.Gson
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.untilNetwork
|
||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
@ -31,17 +34,18 @@ abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adap
|
||||
abstract override fun getItemId(position: Int): Long
|
||||
}
|
||||
|
||||
abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
abstract class LiveOtterFragment<D : Any, DAO : Any, A : OtterAdapter<DAO, *>> : Fragment() {
|
||||
companion object {
|
||||
const val OFFSCREEN_PAGES = 20
|
||||
}
|
||||
|
||||
abstract val liveData: LiveData<List<DAO>>
|
||||
abstract val viewRes: Int
|
||||
abstract val recycler: RecyclerView
|
||||
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
|
||||
open val alwaysRefresh = true
|
||||
|
||||
lateinit var repository: Repository<D, *>
|
||||
lateinit var repository: Repository<D>
|
||||
lateinit var adapter: A
|
||||
|
||||
private var moreLoading = false
|
||||
@ -58,7 +62,7 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
recycler.adapter = adapter
|
||||
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
val offset = recycler.computeVerticalScrollOffset()
|
||||
@ -66,7 +70,7 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) {
|
||||
moreLoading = true
|
||||
|
||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||
fetch(adapter.data.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,17 +82,22 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
if (event is Event.ListingsChanged) {
|
||||
withContext(Main) {
|
||||
swiper?.isRefreshing = true
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
fetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(Repository.Origin.Cache.origin)
|
||||
fetch()
|
||||
}
|
||||
|
||||
if (alwaysRefresh && adapter.data.isEmpty()) {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
liveData.observe(this) {
|
||||
adapter.data = it.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,91 +105,38 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
super.onResume()
|
||||
|
||||
swiper?.setOnRefreshListener {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
fetch()
|
||||
}
|
||||
}
|
||||
|
||||
open fun onDataFetched(data: List<D>) {}
|
||||
|
||||
private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
|
||||
var first = size == 0
|
||||
|
||||
if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
|
||||
lifecycleScope.launch(Main) {
|
||||
swiper?.isRefreshing = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetch(size: Int = 0) {
|
||||
moreLoading = true
|
||||
|
||||
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
|
||||
if (isCache && data.isEmpty()) {
|
||||
moreLoading = false
|
||||
|
||||
return@untilNetwork fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
repository.fetch(size).untilNetwork(lifecycleScope, IO) { data, _, hasMore ->
|
||||
lifecycleScope.launch(Main) {
|
||||
if (isCache) {
|
||||
moreLoading = false
|
||||
|
||||
adapter.data = data.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (first) {
|
||||
adapter.data.clear()
|
||||
}
|
||||
|
||||
onDataFetched(data)
|
||||
|
||||
adapter.data.addAll(data)
|
||||
|
||||
withContext(IO) {
|
||||
try {
|
||||
repository.cacheId?.let { cacheId ->
|
||||
Cache.set(
|
||||
context,
|
||||
cacheId,
|
||||
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||
)
|
||||
}
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMore) {
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
if (first || needsMoreOffscreenPages()) {
|
||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
if (size == 0 || needsMoreOffscreenPages()) {
|
||||
fetch(size + data.size)
|
||||
} else {
|
||||
moreLoading = false
|
||||
}
|
||||
} else {
|
||||
moreLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||
when (upstream.behavior) {
|
||||
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
|
||||
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
|
||||
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
when (first) {
|
||||
true -> {
|
||||
adapter.notifyDataSetChanged()
|
||||
first = false
|
||||
}
|
||||
|
||||
false -> adapter.notifyItemRangeInserted(adapter.data.size, data.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,59 +5,65 @@ import android.view.Gravity
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.PlaylistTracksAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack
|
||||
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.PlaylistTracksRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||
import com.github.apognu.otter.viewmodels.PlaylistViewModel
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.fragment_tracks.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapter>() {
|
||||
class PlaylistTracksFragment : LiveOtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
|
||||
override lateinit var liveData: LiveData<List<Track>>
|
||||
override val viewRes = R.layout.fragment_tracks
|
||||
override val recycler: RecyclerView get() = tracks
|
||||
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
|
||||
var albumId = 0
|
||||
var albumArtist = ""
|
||||
var albumTitle = ""
|
||||
var albumCover = ""
|
||||
var playlistId = 0
|
||||
var playlistName = ""
|
||||
|
||||
companion object {
|
||||
fun new(playlist: Playlist): PlaylistTracksFragment {
|
||||
fun new(playlist: PlaylistEntity): PlaylistTracksFragment {
|
||||
return PlaylistTracksFragment().apply {
|
||||
arguments = bundleOf(
|
||||
"albumId" to playlist.id,
|
||||
"albumArtist" to "N/A",
|
||||
"albumTitle" to playlist.name,
|
||||
"albumCover" to ""
|
||||
"playlistId" to playlist.id,
|
||||
"playlistName" to playlist.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.apply {
|
||||
albumId = getInt("albumId")
|
||||
albumArtist = getString("albumArtist") ?: ""
|
||||
albumTitle = getString("albumTitle") ?: ""
|
||||
albumCover = getString("albumCover") ?: ""
|
||||
playlistId = getInt("playlistId")
|
||||
playlistName = getString("playlistName") ?: "N/A"
|
||||
}
|
||||
|
||||
liveData = PlaylistViewModel(playlistId).tracks
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = PlaylistTracksAdapter(context, FavoriteListener())
|
||||
repository = PlaylistTracksRepository(context, albumId)
|
||||
repository = PlaylistTracksRepository(context, playlistId)
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
|
||||
watchEventBus()
|
||||
PlayerStateViewModel.get().track.observe(this) { track ->
|
||||
adapter.currentTrack = track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -67,19 +73,12 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||
covers.visibility = View.VISIBLE
|
||||
|
||||
artist.text = "Playlist"
|
||||
title.text = albumTitle
|
||||
title.text = playlistName
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response ->
|
||||
adapter.currentTrack = response.track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
var coverHeight: Float? = null
|
||||
|
||||
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
|
||||
@ -95,7 +94,7 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||
}
|
||||
|
||||
play.setOnClickListener {
|
||||
CommandBus.send(Command.ReplaceQueue(adapter.data.map { it.track }.shuffled()))
|
||||
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
@ -108,12 +107,12 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.add_to_queue -> {
|
||||
CommandBus.send(Command.AddToQueue(adapter.data.map { it.track }))
|
||||
CommandBus.send(Command.AddToQueue(adapter.data))
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
|
||||
R.id.download -> CommandBus.send(Command.PinTracks(adapter.data.map { it.track }))
|
||||
R.id.download -> CommandBus.send(Command.PinTracks(adapter.data))
|
||||
}
|
||||
|
||||
true
|
||||
@ -125,8 +124,8 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataFetched(data: List<PlaylistTrack>) {
|
||||
data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url ->
|
||||
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>) {
|
||||
data.map { it.track.album }.toSet().map { it?.cover?.urls?.original }.take(4).forEachIndexed { index, url ->
|
||||
val imageView = when (index) {
|
||||
0 -> cover_top_left
|
||||
1 -> cover_top_right
|
||||
@ -156,23 +155,6 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchEventBus() {
|
||||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
track?.let {
|
||||
adapter.currentTrack = track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
inner class FavoriteListener : PlaylistTracksAdapter.OnFavoriteListener {
|
||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||
when (state) {
|
||||
|
@ -9,12 +9,15 @@ import androidx.transition.Slide
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.adapters.PlaylistsAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhalePlaylist
|
||||
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||
import com.github.apognu.otter.repositories.PlaylistsRepository
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Playlist
|
||||
import com.github.apognu.otter.viewmodels.PlaylistsViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_playlists.*
|
||||
|
||||
class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() {
|
||||
class PlaylistsFragment : LiveOtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
|
||||
override val liveData = PlaylistsViewModel().playlists
|
||||
override val viewRes = R.layout.fragment_playlists
|
||||
override val recycler: RecyclerView get() = playlists
|
||||
override val alwaysRefresh = false
|
||||
@ -27,7 +30,7 @@ class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() {
|
||||
}
|
||||
|
||||
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
|
||||
override fun onClick(holder: View?, playlist: Playlist) {
|
||||
override fun onClick(holder: View?, playlist: PlaylistEntity) {
|
||||
(context as? MainActivity)?.let { activity ->
|
||||
exitTransition = Fade().apply {
|
||||
duration = AppContext.TRANSITION_DURATION
|
||||
|
@ -6,25 +6,26 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.TracksAdapter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import com.github.apognu.otter.viewmodels.QueueViewModel
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_queue.*
|
||||
import kotlinx.android.synthetic.main.fragment_queue.view.*
|
||||
import kotlinx.android.synthetic.main.partial_queue.*
|
||||
import kotlinx.android.synthetic.main.partial_queue.view.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class QueueFragment : BottomSheetDialogFragment() {
|
||||
private var adapter: TracksAdapter? = null
|
||||
|
||||
private val viewModel = QueueViewModel.get()
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -33,8 +34,6 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet)
|
||||
|
||||
watchEventBus()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@ -48,6 +47,10 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
viewModel.queue.observe(viewLifecycleOwner) {
|
||||
refresh(it)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.fragment_queue, container, false).apply {
|
||||
adapter = TracksAdapter(context, FavoriteListener(), fromQueue = true).also {
|
||||
included.queue.layoutManager = LinearLayoutManager(context)
|
||||
@ -69,44 +72,20 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||
queue_clear.setOnClickListener {
|
||||
CommandBus.send(Command.ClearQueue)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
lifecycleScope.launch(Main) {
|
||||
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
|
||||
included?.let { included ->
|
||||
adapter?.let {
|
||||
it.data = response.queue.toMutableList()
|
||||
it.notifyDataSetChanged()
|
||||
private fun refresh(tracks: List<Track>) {
|
||||
included?.let { included ->
|
||||
adapter?.let {
|
||||
it.data = tracks.toMutableList()
|
||||
it.notifyDataSetChanged()
|
||||
|
||||
if (it.data.isEmpty()) {
|
||||
included.queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
} else {
|
||||
included.queue?.visibility = View.VISIBLE
|
||||
placeholder?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun watchEventBus() {
|
||||
lifecycleScope.launch(Main) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.QueueChanged -> refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshTrack -> refresh()
|
||||
if (it.data.isEmpty()) {
|
||||
included.queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
} else {
|
||||
included.queue?.visibility = View.VISIBLE
|
||||
placeholder?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,18 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.RadiosAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
import com.github.apognu.otter.repositories.RadiosRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.RadiosViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_radios.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
|
||||
class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
|
||||
override val liveData = RadiosViewModel().radios
|
||||
override val viewRes = R.layout.fragment_radios
|
||||
override val recycler: RecyclerView get() = radios
|
||||
override val alwaysRefresh = false
|
||||
@ -26,13 +30,14 @@ class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
|
||||
}
|
||||
|
||||
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
|
||||
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: Radio) {
|
||||
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: RadioEntity) {
|
||||
holder.spin()
|
||||
recycler.forEach {
|
||||
it.isEnabled = false
|
||||
it.isClickable = false
|
||||
}
|
||||
|
||||
// TOBEREDONE
|
||||
CommandBus.send(Command.PlayRadio(radio))
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
|
@ -11,9 +11,9 @@ import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.mustNormalizeUrl
|
||||
import com.github.apognu.otter.utils.toDurationString
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import kotlinx.android.synthetic.main.fragment_track_info_details.*
|
||||
|
||||
class TrackInfoDetailsFragment : DialogFragment() {
|
||||
@ -21,7 +21,7 @@ class TrackInfoDetailsFragment : DialogFragment() {
|
||||
fun new(track: Track): TrackInfoDetailsFragment {
|
||||
return TrackInfoDetailsFragment().apply {
|
||||
arguments = bundleOf(
|
||||
"artistName" to track.artist.name,
|
||||
"artistName" to track.artist?.name,
|
||||
"albumTitle" to track.album?.title,
|
||||
"trackTitle" to track.title,
|
||||
"trackCopyright" to track.copyright,
|
||||
|
@ -6,14 +6,20 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.TracksAdapter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.TracksRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.*
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.preference.PowerPreference
|
||||
import com.squareup.picasso.Picasso
|
||||
@ -25,7 +31,8 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>() {
|
||||
override lateinit var liveData: LiveData<List<Track>>
|
||||
override val viewRes = R.layout.fragment_tracks
|
||||
override val recycler: RecyclerView get() = tracks
|
||||
|
||||
@ -40,26 +47,36 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
companion object {
|
||||
fun new(album: Album): TracksFragment {
|
||||
return TracksFragment().apply {
|
||||
arguments = bundleOf(
|
||||
"albumId" to album.id,
|
||||
"albumArtist" to album.artist.name,
|
||||
"albumTitle" to album.title,
|
||||
"albumCover" to album.cover()
|
||||
)
|
||||
arguments = bundleOf("albumId" to album.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.apply {
|
||||
albumId = getInt("albumId")
|
||||
albumArtist = getString("albumArtist") ?: ""
|
||||
albumTitle = getString("albumTitle") ?: ""
|
||||
albumCover = getString("albumCover") ?: ""
|
||||
}
|
||||
|
||||
liveData = TracksViewModel(albumId).tracks
|
||||
|
||||
AlbumViewModel(albumId).album.observe(this) {
|
||||
title.text = it.title
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(it.cover))
|
||||
.noFade()
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(cover)
|
||||
|
||||
ArtistViewModel(it.artist_id).artist.observe(this) {
|
||||
artist.text = it.name
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = TracksAdapter(context, FavoriteListener())
|
||||
repository = TracksRepository(context, albumId)
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
@ -68,33 +85,9 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
watchEventBus()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(albumCover))
|
||||
.noFade()
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(cover)
|
||||
|
||||
artist.text = albumArtist
|
||||
title.text = albumTitle
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response ->
|
||||
adapter.currentTrack = response.track
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
refreshDownloadedTracks()
|
||||
}
|
||||
|
||||
var coverHeight: Float? = null
|
||||
|
||||
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
|
||||
@ -171,24 +164,16 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshDownloadedTracks() {
|
||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
|
||||
withContext(Main) {
|
||||
adapter.data = adapter.data.map {
|
||||
/* adapter.data = adapter.data.map {
|
||||
it.downloaded = downloaded.contains(it.id)
|
||||
it
|
||||
}.toMutableList()
|
||||
}.toMutableList() */
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
@ -198,26 +183,15 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
download.getMetadata()?.let { info ->
|
||||
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||
withContext(Main) {
|
||||
/* withContext(Main) {
|
||||
adapter.data[match.second].downloaded = true
|
||||
adapter.notifyItemChanged(match.second)
|
||||
}
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
track?.let {
|
||||
adapter.currentTrack?.current = false
|
||||
adapter.currentTrack = track.apply {
|
||||
current = true
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||
when (state) {
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FunkwhaleAlbum(
|
||||
val id: Int,
|
||||
val artist: Artist,
|
||||
val title: String,
|
||||
val cover: Covers?,
|
||||
val release_date: String?
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Artist(val id: Int, val name: String)
|
||||
|
||||
fun cover() = cover?.urls?.original
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Covers(val urls: CoverUrls?)
|
||||
|
||||
@Serializable
|
||||
data class CoverUrls(val original: String?)
|
@ -0,0 +1,19 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FunkwhaleArtist(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val albums: List<Album>? = null
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Album(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val cover: Covers?,
|
||||
val release_date: String?
|
||||
)
|
||||
}
|
27
app/src/main/java/com/github/apognu/otter/models/api/Base.kt
Normal file
27
app/src/main/java/com/github/apognu/otter/models/api/Base.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
@Serializable
|
||||
data class OtterResponse<D : Any>(
|
||||
val count: Int,
|
||||
val next: String? = null,
|
||||
val results: List<D>
|
||||
)
|
||||
|
||||
@Serializer(forClass = OtterResponse::class)
|
||||
class OtterResponseSerializer<T : Any>(private val dataSerializer: KSerializer<T>) : KSerializer<OtterResponse<T>> {
|
||||
override val descriptor = PrimitiveDescriptor("OtterResponse", kind = PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: OtterResponse<T>) {}
|
||||
|
||||
override fun deserialize(decoder: Decoder): OtterResponse<T> {
|
||||
return OtterResponse.serializer(dataSerializer).deserialize(decoder)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Credentials(val token: String, val non_field_errors: List<String>? = null)
|
||||
|
||||
@Serializable
|
||||
data class User(val full_username: String)
|
@ -0,0 +1,15 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FunkwhalePlaylist(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val album_covers: List<String>,
|
||||
val tracks_count: Int,
|
||||
val duration: Int?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FunkwhalePlaylistTrack(val track: FunkwhaleTrack)
|
@ -0,0 +1,12 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FunkwhaleRadio(
|
||||
val id: Int,
|
||||
var radio_type: String? = null,
|
||||
val name: String,
|
||||
val description: String,
|
||||
var related_object_id: String? = null
|
||||
)
|
@ -0,0 +1,64 @@
|
||||
package com.github.apognu.otter.models.api
|
||||
|
||||
import com.github.apognu.otter.models.domain.SearchResult
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FunkwhaleTrack(
|
||||
val id: Int = 0,
|
||||
val title: String,
|
||||
val artist: FunkwhaleArtist,
|
||||
val album: FunkwhaleAlbum?,
|
||||
val disc_number: Int? = null,
|
||||
val position: Int = 0,
|
||||
val uploads: List<FunkwhaleUpload> = listOf(),
|
||||
val copyright: String? = null,
|
||||
val license: String? = null
|
||||
) : SearchResult {
|
||||
var current: Boolean = false
|
||||
var favorite: Boolean = false
|
||||
var cached: Boolean = false
|
||||
var downloaded: Boolean = false
|
||||
|
||||
companion object {
|
||||
fun fromDownload(download: DownloadInfo): FunkwhaleTrack = FunkwhaleTrack(
|
||||
id = download.id,
|
||||
title = download.title,
|
||||
artist = FunkwhaleArtist(0, download.artist, listOf()),
|
||||
album = FunkwhaleAlbum(0, FunkwhaleAlbum.Artist(0, ""), "", Covers(CoverUrls("")), ""),
|
||||
uploads = listOf(FunkwhaleUpload(download.contentId, 0, 0))
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class FunkwhaleUpload(
|
||||
val listen_url: String,
|
||||
val duration: Int,
|
||||
val bitrate: Int
|
||||
)
|
||||
|
||||
override fun hashCode() = id
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return when (other) {
|
||||
is FunkwhaleTrack -> other.id == id
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun cover() = album?.cover()
|
||||
override fun title() = title
|
||||
override fun subtitle() = artist.name
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Favorited(val track: Int)
|
||||
|
||||
data class DownloadInfo(
|
||||
val id: Int,
|
||||
val contentId: String,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
var download: Download?
|
||||
)
|
@ -0,0 +1,66 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||
|
||||
@Entity(tableName = "albums")
|
||||
data class AlbumEntity(
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
val title: String,
|
||||
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = ForeignKey.CASCADE)
|
||||
val artist_id: Int,
|
||||
val cover: String?,
|
||||
val release_date: String?
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity")
|
||||
fun allDecorated(): LiveData<List<DecoratedAlbumEntity>>
|
||||
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY release_date")
|
||||
fun allSync(): List<DecoratedAlbumEntity>
|
||||
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id IN ( :ids ) ORDER BY release_date")
|
||||
fun findAllDecorated(ids: List<Int>): LiveData<List<DecoratedAlbumEntity>>
|
||||
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
||||
fun getDecorated(id: Int): LiveData<DecoratedAlbumEntity>
|
||||
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
||||
fun getDecoratedBlocking(id: Int): DecoratedAlbumEntity
|
||||
|
||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE artist_id = :artistId")
|
||||
fun forArtistDecorated(artistId: Int): LiveData<List<DecoratedAlbumEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(album: AlbumEntity)
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhaleAlbum.toDao() = run {
|
||||
AlbumEntity(id, title, artist.id, cover(), release_date)
|
||||
}
|
||||
|
||||
fun FunkwhaleArtist.Album.toDao(artistId: Int) = run {
|
||||
AlbumEntity(id, title, artistId, cover?.urls?.original, release_date)
|
||||
}
|
||||
|
||||
@DatabaseView("""
|
||||
SELECT albums.*, artists.name AS artist_name
|
||||
FROM albums
|
||||
INNER JOIN artists
|
||||
ON artists.id = albums.artist_id
|
||||
ORDER BY albums.release_date
|
||||
""")
|
||||
data class DecoratedAlbumEntity(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val artist_id: Int,
|
||||
val cover: String?,
|
||||
val release_date: String?,
|
||||
val artist_name: String
|
||||
)
|
@ -0,0 +1,64 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Required
|
||||
|
||||
@Entity(tableName = "artists")
|
||||
data class ArtistEntity(
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
@ColumnInfo(collate = ColumnInfo.LOCALIZED, index = true)
|
||||
val name: String
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM DecoratedArtistEntity")
|
||||
fun allDecorated(): LiveData<List<DecoratedArtistEntity>>
|
||||
|
||||
@Query("SELECT * FROM DecoratedArtistEntity WHERE id == :id")
|
||||
fun getDecorated(id: Int): LiveData<DecoratedArtistEntity>
|
||||
|
||||
@Query("SELECT * FROM DecoratedArtistEntity WHERE id == :id")
|
||||
fun getDecoratedBlocking(id: Int): DecoratedArtistEntity
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(artist: ArtistEntity)
|
||||
|
||||
@Query("DELETE FROM artists")
|
||||
fun deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhaleArtist.toDao() = run {
|
||||
ArtistEntity(id, name)
|
||||
}
|
||||
|
||||
@DatabaseView("""
|
||||
SELECT artists.id, artists.name, COUNT(*) AS album_count, albums.cover AS album_cover
|
||||
FROM artists
|
||||
INNER JOIN albums
|
||||
ON albums.artist_id = artists.id
|
||||
GROUP BY albums.artist_id
|
||||
ORDER BY name
|
||||
""")
|
||||
data class DecoratedArtistEntity(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val album_count: Int,
|
||||
val album_cover: String?
|
||||
)
|
||||
|
||||
open class RealmArtist(
|
||||
@io.realm.annotations.PrimaryKey
|
||||
var id: Int = 0,
|
||||
@Required
|
||||
var name: String = ""
|
||||
) : RealmObject()
|
||||
|
||||
fun FunkwhaleArtist.toRealmDao() = run {
|
||||
RealmArtist(id, name)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
|
||||
@Entity(tableName = "favorites")
|
||||
data class FavoriteEntity(
|
||||
@PrimaryKey
|
||||
val track_id: Int
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM favorites")
|
||||
fun all(): LiveData<List<FavoriteEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(trackId: FavoriteEntity)
|
||||
|
||||
@Query("INSERT OR REPLACE INTO favorites VALUES ( :trackId )")
|
||||
fun add(trackId: Int)
|
||||
|
||||
@Query("DELETE FROM favorites WHERE track_id = :trackId")
|
||||
fun remove(trackId: Int)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [
|
||||
ArtistEntity::class,
|
||||
AlbumEntity::class,
|
||||
TrackEntity::class,
|
||||
UploadEntity::class,
|
||||
QueueItemEntity::class,
|
||||
PlaylistEntity::class,
|
||||
PlaylistTrack::class,
|
||||
RadioEntity::class,
|
||||
FavoriteEntity::class
|
||||
],
|
||||
views = [
|
||||
DecoratedArtistEntity::class,
|
||||
DecoratedAlbumEntity::class,
|
||||
DecoratedTrackEntity::class
|
||||
]
|
||||
)
|
||||
@TypeConverters(StringListConverter::class)
|
||||
abstract class OtterDatabase : RoomDatabase() {
|
||||
abstract fun artists(): ArtistEntity.Dao
|
||||
abstract fun albums(): AlbumEntity.Dao
|
||||
abstract fun tracks(): TrackEntity.Dao
|
||||
abstract fun uploads(): UploadEntity.Dao
|
||||
|
||||
abstract fun queue(): QueueItemEntity.Dao
|
||||
|
||||
abstract fun playlists(): PlaylistEntity.Dao
|
||||
abstract fun radios(): RadioEntity.Dao
|
||||
abstract fun favorites(): FavoriteEntity.Dao
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.api.FunkwhalePlaylist
|
||||
|
||||
@Entity(tableName = "playlists")
|
||||
data class PlaylistEntity(
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val album_covers: List<String>,
|
||||
val tracks_count: Int,
|
||||
val duration: Int
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM playlists ORDER BY name")
|
||||
fun all(): LiveData<List<PlaylistEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE id IN ( SELECT track_id FROM playlist_tracks WHERE playlist_id = :id )")
|
||||
fun tracksFor(id: Int): LiveData<List<DecoratedTrackEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(playlist: PlaylistEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun addTracks(tracks: List<PlaylistTrack>)
|
||||
|
||||
@Query("DELETE FROM playlist_tracks WHERE playlist_id = :id")
|
||||
fun deleteTracksFor(id: Int)
|
||||
|
||||
@Transaction
|
||||
fun replaceTracks(id: Int, tracks: List<PlaylistTrack>) {
|
||||
deleteTracksFor(id)
|
||||
addTracks(tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhalePlaylist.toDao(): PlaylistEntity = run {
|
||||
PlaylistEntity(id, name, album_covers, tracks_count, duration ?: 0)
|
||||
}
|
||||
|
||||
@Entity(tableName = "playlist_tracks", primaryKeys = ["playlist_id", "track_id"])
|
||||
data class PlaylistTrack(
|
||||
val playlist_id: Int,
|
||||
val track_id: Int
|
||||
)
|
||||
|
||||
object StringListConverter {
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromString(value: String): List<String> {
|
||||
return value.split(",").toList()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun toString(value: List<String>): String {
|
||||
return value.joinToString(",")
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
|
||||
@Entity(tableName = "queue")
|
||||
data class QueueItemEntity(
|
||||
@PrimaryKey
|
||||
val position: Int,
|
||||
@ForeignKey(entity = TrackEntity::class, parentColumns = ["id"], childColumns = ["track_id"], onDelete = ForeignKey.CASCADE)
|
||||
val trackId: Int
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT tracks.*
|
||||
FROM DecoratedTrackEntity tracks
|
||||
INNER JOIN queue
|
||||
ON queue.trackId = tracks.id
|
||||
ORDER BY queue.position
|
||||
""")
|
||||
fun allDecorated(): LiveData<List<DecoratedTrackEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT tracks.*
|
||||
FROM DecoratedTrackEntity tracks
|
||||
INNER JOIN queue
|
||||
ON queue.trackId = tracks.id
|
||||
ORDER BY queue.position
|
||||
""")
|
||||
fun allDecoratedBlocking(): List<DecoratedTrackEntity>
|
||||
|
||||
@Query("DELETE FROM queue")
|
||||
fun empty()
|
||||
|
||||
@Insert
|
||||
fun insertAll(tracks: List<QueueItemEntity>)
|
||||
|
||||
@Transaction
|
||||
fun replace(tracks: List<Track>) {
|
||||
empty()
|
||||
insertAll(tracks.mapIndexed { position, track ->
|
||||
track.toQueueItemDao(position)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Track.toQueueItemDao(position: Int = 0): QueueItemEntity = run {
|
||||
QueueItemEntity(position, id)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
||||
|
||||
@Entity(tableName = "radios")
|
||||
data class RadioEntity(
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
var radio_type: String?,
|
||||
val name: String,
|
||||
val description: String,
|
||||
var related_object_id: String? = null
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM radios ORDER BY name")
|
||||
fun all(): LiveData<List<RadioEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(radio: RadioEntity)
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhaleRadio.toDao(): RadioEntity = run {
|
||||
RadioEntity(id, radio_type, name, description, related_object_id)
|
||||
}
|
124
app/src/main/java/com/github/apognu/otter/models/dao/Track.kt
Normal file
124
app/src/main/java/com/github/apognu/otter/models/dao/Track.kt
Normal file
@ -0,0 +1,124 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import androidx.room.ForeignKey.CASCADE
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
|
||||
@Entity(tableName = "tracks")
|
||||
data class TrackEntity(
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
val title: String,
|
||||
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = CASCADE)
|
||||
val artist_id: Int,
|
||||
@ForeignKey(entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["album_id"], onDelete = CASCADE)
|
||||
val album_id: Int?,
|
||||
val position: Int?,
|
||||
val copyright: String?,
|
||||
val license: String?
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE id = :id")
|
||||
fun find(id: Int): DecoratedTrackEntity
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE id IN ( :ids )")
|
||||
fun findAllDecorated(ids: List<Int>): LiveData<List<DecoratedTrackEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE id = :id")
|
||||
fun getDecorated(id: Int): LiveData<DecoratedTrackEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE id = :id")
|
||||
fun getDecoratedBlocking(id: Int): DecoratedTrackEntity
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE album_id IN ( :albumIds )")
|
||||
fun ofAlbumsDecorated(albumIds: List<Int>): LiveData<List<DecoratedTrackEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DecoratedTrackEntity WHERE artist_id = :artistId")
|
||||
suspend fun ofArtistBlocking(artistId: Int): List<DecoratedTrackEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("""
|
||||
SELECT tracks.*
|
||||
FROM DecoratedTrackEntity tracks
|
||||
INNER JOIN favorites
|
||||
WHERE favorites.track_id = tracks.id
|
||||
""")
|
||||
fun favorites(): LiveData<List<DecoratedTrackEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(track: TrackEntity)
|
||||
|
||||
@Transaction
|
||||
fun insertWithAssocs(track: FunkwhaleTrack) {
|
||||
Otter.get().database.artists().insert(track.artist.toDao())
|
||||
|
||||
track.album?.let {
|
||||
Otter.get().database.albums().insert(it.toDao())
|
||||
}
|
||||
|
||||
insert(track.toDao())
|
||||
|
||||
track.uploads.forEach {
|
||||
Otter.get().database.uploads().insert(it.toDao(track.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhaleTrack.toDao() = run {
|
||||
TrackEntity(
|
||||
id,
|
||||
title,
|
||||
artist.id,
|
||||
album?.id,
|
||||
position,
|
||||
copyright,
|
||||
license
|
||||
)
|
||||
}
|
||||
|
||||
@DatabaseView("""
|
||||
SELECT
|
||||
tracks.id, tracks.title, tracks.position, tracks.copyright, tracks.license,
|
||||
ar.id AS artist_id, ar.name AS artist_name, ar.album_count AS artist_album_count, ar.album_cover AS artist_album_cover,
|
||||
al.id AS album_id, al.title AS album_title, al.artist_id AS album_artist_id, al.cover AS album_cover, al.release_date AS album_release_date, al.artist_name AS album_artist_name,
|
||||
CASE
|
||||
WHEN favorites.track_id IS NULL THEN 0
|
||||
ELSE 1
|
||||
END AS favorite
|
||||
FROM tracks
|
||||
LEFT JOIN DecoratedAlbumEntity al
|
||||
ON al.id = tracks.album_id
|
||||
LEFT JOIN DecoratedArtistEntity ar
|
||||
ON ar.id = al.artist_id
|
||||
LEFT JOIN favorites
|
||||
ON favorites.track_id = tracks.id
|
||||
""")
|
||||
data class DecoratedTrackEntity(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val position: Int?,
|
||||
val copyright: String?,
|
||||
val license: String?,
|
||||
|
||||
// Virtual attributes
|
||||
val favorite: Boolean,
|
||||
|
||||
// Associations
|
||||
@Embedded(prefix = "artist_")
|
||||
val artist: DecoratedArtistEntity?,
|
||||
@Embedded(prefix = "album_")
|
||||
val album: DecoratedAlbumEntity?,
|
||||
@Relation(entityColumn = "track_id", parentColumn = "id")
|
||||
val uploads: List<UploadEntity>
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package com.github.apognu.otter.models.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
|
||||
@Entity(tableName = "uploads")
|
||||
data class UploadEntity(
|
||||
@PrimaryKey
|
||||
val listen_url: String,
|
||||
@ForeignKey(entity = TrackEntity::class, parentColumns = ["id"], childColumns = ["track_id"], onDelete = ForeignKey.CASCADE)
|
||||
val track_id: Int,
|
||||
val duration: Int,
|
||||
val bitrate: Int
|
||||
) {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
@Query("SELECT * FROM uploads WHERE track_id IN ( :ids )")
|
||||
fun findAll(ids: List<Int>): LiveData<List<UploadEntity>>
|
||||
|
||||
@Query("SELECT * FROM uploads WHERE track_id IN ( :ids )")
|
||||
suspend fun findAllBlocking(ids: List<Int>): List<UploadEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(upload: UploadEntity)
|
||||
}
|
||||
}
|
||||
|
||||
fun FunkwhaleTrack.FunkwhaleUpload.toDao(trackId: Int): UploadEntity = run {
|
||||
UploadEntity(
|
||||
listen_url,
|
||||
trackId,
|
||||
duration,
|
||||
bitrate
|
||||
)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.github.apognu.otter.models.domain
|
||||
|
||||
import com.github.apognu.otter.models.dao.DecoratedAlbumEntity
|
||||
|
||||
data class Album(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val artist_id: Int,
|
||||
val cover: String? = null,
|
||||
val release_date: String? = null,
|
||||
var artist_name: String = ""
|
||||
): SearchResult {
|
||||
|
||||
companion object {
|
||||
fun fromDecoratedEntity(entity: DecoratedAlbumEntity): Album = entity.run {
|
||||
Album(
|
||||
id,
|
||||
title,
|
||||
artist_id,
|
||||
cover,
|
||||
release_date,
|
||||
artist_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cover() = cover
|
||||
override fun title() = title
|
||||
override fun subtitle() = artist_name
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.github.apognu.otter.models.domain
|
||||
|
||||
import com.github.apognu.otter.models.dao.DecoratedArtistEntity
|
||||
|
||||
data class Artist(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val album_count: Int = 0,
|
||||
val album_cover: String? = "",
|
||||
var albums: List<Album> = listOf()
|
||||
) : SearchResult {
|
||||
|
||||
companion object {
|
||||
fun fromDecoratedEntity(entity: DecoratedArtistEntity): Artist = entity.run {
|
||||
Artist(
|
||||
id,
|
||||
name,
|
||||
album_count = album_count,
|
||||
album_cover = album_cover
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cover() = album_cover
|
||||
override fun title() = name
|
||||
override fun subtitle() = "Artist"
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.github.apognu.otter.models.domain
|
||||
|
||||
interface SearchResult {
|
||||
fun cover(): String?
|
||||
fun title(): String
|
||||
fun subtitle(): String
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.github.apognu.otter.models.domain
|
||||
|
||||
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
|
||||
import com.preference.PowerPreference
|
||||
|
||||
data class Track(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val position: Int?,
|
||||
val copyright: String?,
|
||||
val license: String?,
|
||||
|
||||
// Virtual attributes
|
||||
val favorite: Boolean,
|
||||
var current: Boolean = false,
|
||||
var cached: Boolean = false,
|
||||
var downloaded: Boolean = false,
|
||||
|
||||
// Associations
|
||||
val artist: Artist? = null,
|
||||
val album: Album? = null,
|
||||
var uploads: List<Upload> = listOf()
|
||||
) : SearchResult {
|
||||
|
||||
companion object {
|
||||
fun fromDecoratedEntity(entity: DecoratedTrackEntity) = entity.run {
|
||||
Track(
|
||||
id,
|
||||
title,
|
||||
position,
|
||||
copyright,
|
||||
license,
|
||||
favorite,
|
||||
artist = entity.artist?.let { Artist.fromDecoratedEntity(it) },
|
||||
album = entity.album?.let { Album.fromDecoratedEntity(it) },
|
||||
uploads = uploads.map { Upload.fromEntity(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cover() = album?.cover
|
||||
override fun title() = title
|
||||
override fun subtitle() = album?.title ?: "N/A"
|
||||
|
||||
fun bestUpload(): Upload? {
|
||||
if (uploads.isEmpty()) return null
|
||||
|
||||
return when (PowerPreference.getDefaultFile().getString("media_cache_quality")) {
|
||||
"quality" -> uploads.maxBy { it.bitrate } ?: uploads[0]
|
||||
"size" -> uploads.minBy { it.bitrate } ?: uploads[0]
|
||||
else -> uploads.maxBy { it.bitrate } ?: uploads[0]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.github.apognu.otter.models.domain
|
||||
|
||||
import com.github.apognu.otter.models.dao.UploadEntity
|
||||
|
||||
data class Upload(
|
||||
val listen_url: String,
|
||||
val track_id: Int,
|
||||
val duration: Int,
|
||||
val bitrate: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun fromEntity(upload: UploadEntity): Upload = upload.run {
|
||||
Upload(
|
||||
listen_url,
|
||||
track_id,
|
||||
duration,
|
||||
bitrate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
@ -68,7 +68,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||
this
|
||||
}
|
||||
.setContentTitle(track.title)
|
||||
.setContentText(track.artist.name)
|
||||
.setContentText(track.artist?.name)
|
||||
.setContentIntent(openPendingIntent)
|
||||
.setChannelId(AppContext.NOTIFICATION_CHANNEL_MEDIA_CONTROL)
|
||||
.addAction(
|
||||
|
@ -6,7 +6,10 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.api.DownloadInfo
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.DownloadsViewModel
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.android.exoplayer2.offline.DownloadManager
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest
|
||||
@ -17,8 +20,6 @@ import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||
@ -33,7 +34,7 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||
track.id,
|
||||
url,
|
||||
track.title,
|
||||
track.artist.name,
|
||||
track.artist?.name ?: "",
|
||||
null
|
||||
)
|
||||
).toByteArray()
|
||||
@ -48,17 +49,15 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
buildResumeDownloadsIntent(this, PinService::class.java, true)
|
||||
|
||||
scope.launch(Main) {
|
||||
RequestBus.get().collect { request ->
|
||||
when (request) {
|
||||
is Request.GetDownloads -> request.channel?.offer(Response.Downloads(getDownloads()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
DownloadsViewModel.get().cursor.postValue(getDownloads())
|
||||
}
|
||||
|
||||
override fun getDownloadManager() = Otter.get().exoDownloadManager.apply {
|
||||
addListener(DownloadListener())
|
||||
}
|
||||
@ -77,7 +76,15 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download) {
|
||||
super.onDownloadChanged(downloadManager, download)
|
||||
|
||||
EventBus.send(Event.DownloadChanged(download))
|
||||
if (download.state != Download.STATE_REMOVING) {
|
||||
EventBus.send(Event.DownloadChanged(download))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDownloadRemoved(downloadManager: DownloadManager, download: Download) {
|
||||
super.onDownloadRemoved(downloadManager, download)
|
||||
|
||||
DownloadsViewModel.get().cursor.postValue(getDownloads())
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,10 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import com.google.android.exoplayer2.Player
|
||||
@ -133,7 +136,7 @@ class PlayerService : Service() {
|
||||
|
||||
val (current, duration, percent) = getProgress(true)
|
||||
|
||||
ProgressBus.send(current, duration, percent)
|
||||
PlayerStateViewModel.get().position.postValue(Triple(current, duration, percent))
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,11 +148,9 @@ class PlayerService : Service() {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshService -> {
|
||||
EventBus.send(Event.QueueChanged)
|
||||
|
||||
if (queue.metadata.isNotEmpty()) {
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
EventBus.send(Event.StateChanged(player.playWhenReady))
|
||||
PlayerStateViewModel.get()._track.postValue(queue.current())
|
||||
PlayerStateViewModel.get().isPlaying.postValue(player.playWhenReady)
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,6 +194,7 @@ class PlayerService : Service() {
|
||||
|
||||
is Command.PlayRadio -> {
|
||||
queue.clear()
|
||||
// TOBEREDONE
|
||||
radioPlayer.play(command.radio)
|
||||
}
|
||||
|
||||
@ -204,24 +206,12 @@ class PlayerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Main) {
|
||||
RequestBus.get().collect { request ->
|
||||
when (request) {
|
||||
is Request.GetCurrentTrack -> request.channel?.offer(Response.CurrentTrack(queue.current()))
|
||||
is Request.GetState -> request.channel?.offer(Response.State(player.playWhenReady))
|
||||
is Request.GetQueue -> request.channel?.offer(Response.Queue(queue.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Main) {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
|
||||
val (current, duration, percent) = getProgress()
|
||||
|
||||
if (player.playWhenReady) {
|
||||
ProgressBus.send(current, duration, percent)
|
||||
PlayerStateViewModel.get().position.postValue(getProgress())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,7 +271,7 @@ class PlayerService : Service() {
|
||||
if (hasAudioFocus(state)) {
|
||||
player.playWhenReady = state
|
||||
|
||||
EventBus.send(Event.StateChanged(state))
|
||||
PlayerStateViewModel.get().isPlaying.postValue(state)
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +291,7 @@ class PlayerService : Service() {
|
||||
player.next()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||
ProgressBus.send(0, 0, 0)
|
||||
PlayerStateViewModel.get().position.postValue(Triple(0, 0, 0))
|
||||
}
|
||||
|
||||
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||
@ -331,7 +321,7 @@ class PlayerService : Service() {
|
||||
|
||||
return mediaMetadataBuilder.apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist?.name)
|
||||
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||
|
||||
try {
|
||||
@ -381,24 +371,22 @@ class PlayerService : Service() {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
||||
|
||||
EventBus.send(Event.StateChanged(playWhenReady))
|
||||
PlayerStateViewModel.get().isPlaying.postValue(playWhenReady)
|
||||
|
||||
if (queue.current == -1) {
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
PlayerStateViewModel.get()._track.postValue(queue.current())
|
||||
}
|
||||
|
||||
when (playWhenReady) {
|
||||
true -> {
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
|
||||
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
|
||||
Player.STATE_BUFFERING -> PlayerStateViewModel.get().isBuffering.postValue(true)
|
||||
Player.STATE_ENDED -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
queue.current = 0
|
||||
player.seekTo(0, C.TIME_UNSET)
|
||||
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
|
||||
Player.STATE_IDLE -> {
|
||||
@ -408,11 +396,11 @@ class PlayerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
|
||||
if (playbackState != Player.STATE_BUFFERING) PlayerStateViewModel.get().isBuffering.postValue(false)
|
||||
}
|
||||
|
||||
false -> {
|
||||
EventBus.send(Event.Buffering(false))
|
||||
PlayerStateViewModel.get().isBuffering.postValue(false)
|
||||
|
||||
Build.VERSION_CODES.N.onApi(
|
||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||
@ -446,7 +434,7 @@ class PlayerService : Service() {
|
||||
|
||||
Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray())
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
PlayerStateViewModel.get()._track.postValue(queue.current())
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(reason: Int) {
|
||||
|
@ -4,8 +4,10 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.QueueRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||
@ -13,9 +15,13 @@ import com.google.android.exoplayer2.upstream.FileDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class QueueManager(val context: Context) {
|
||||
private val queueRepository = QueueRepository(GlobalScope)
|
||||
|
||||
var metadata: MutableList<Track> = mutableListOf()
|
||||
val datasources = ConcatenatingMediaSource()
|
||||
var current = -1
|
||||
@ -44,34 +50,24 @@ class QueueManager(val context: Context) {
|
||||
}
|
||||
|
||||
init {
|
||||
Cache.get(context, "queue")?.let { json ->
|
||||
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
||||
metadata = cache.data.toMutableList()
|
||||
|
||||
val factory = factory(context)
|
||||
|
||||
datasources.addMediaSources(metadata.map { track ->
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
||||
})
|
||||
GlobalScope.launch(IO) {
|
||||
queueRepository.allBlocking().also {
|
||||
replace(it.map { Track.fromDecoratedEntity(it) })
|
||||
}
|
||||
}
|
||||
|
||||
Cache.get(context, "current")?.let { string ->
|
||||
current = string.readLine().toInt()
|
||||
|
||||
PlayerStateViewModel.get()._track.postValue(current())
|
||||
}
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
Cache.set(
|
||||
context,
|
||||
"queue",
|
||||
Gson().toJson(QueueCache(metadata)).toByteArray()
|
||||
)
|
||||
}
|
||||
private fun persist() = queueRepository.replace(metadata)
|
||||
|
||||
fun replace(tracks: List<Track>) {
|
||||
metadata = tracks.toMutableList()
|
||||
|
||||
val factory = factory(context)
|
||||
|
||||
val sources = tracks.map { track ->
|
||||
@ -80,13 +76,10 @@ class QueueManager(val context: Context) {
|
||||
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
||||
}
|
||||
|
||||
metadata = tracks.toMutableList()
|
||||
datasources.clear()
|
||||
datasources.addMediaSources(sources)
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
|
||||
fun append(tracks: List<Track>) {
|
||||
@ -103,8 +96,6 @@ class QueueManager(val context: Context) {
|
||||
datasources.addMediaSources(sources)
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
|
||||
fun insertNext(track: Track) {
|
||||
@ -121,8 +112,6 @@ class QueueManager(val context: Context) {
|
||||
}
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
|
||||
fun remove(track: Track) {
|
||||
@ -148,8 +137,6 @@ class QueueManager(val context: Context) {
|
||||
}
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
|
||||
fun move(oldPosition: Int, newPosition: Int) {
|
||||
@ -159,10 +146,7 @@ class QueueManager(val context: Context) {
|
||||
persist()
|
||||
}
|
||||
|
||||
fun get() = metadata.mapIndexed { index, track ->
|
||||
track.current = index == current
|
||||
track
|
||||
}
|
||||
fun get() = metadata
|
||||
|
||||
fun get(index: Int): Track = metadata[index]
|
||||
|
||||
@ -205,7 +189,5 @@ class QueueManager(val context: Context) {
|
||||
}
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
}
|
@ -1,34 +1,44 @@
|
||||
package com.github.apognu.otter.playback
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null, var related_object_id: String? = null)
|
||||
@Serializable
|
||||
data class RadioSessionBody(val radio_type: String?, var custom_radio: Int? = null, var related_object_id: String? = null)
|
||||
|
||||
@Serializable
|
||||
data class RadioSession(val id: Int)
|
||||
|
||||
@Serializable
|
||||
data class RadioTrackBody(val session: Int)
|
||||
|
||||
@Serializable
|
||||
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
||||
|
||||
@Serializable
|
||||
data class RadioTrackID(val id: Int)
|
||||
|
||||
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
val lock = Semaphore(1)
|
||||
|
||||
private var currentRadio: Radio? = null
|
||||
private var currentRadio: RadioEntity? = null
|
||||
private var session: Int? = null
|
||||
private var cookie: String? = null
|
||||
|
||||
@ -40,7 +50,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
||||
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
|
||||
|
||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||
currentRadio = RadioEntity(radio_id, radio_type, "", "")
|
||||
session = radio_session
|
||||
cookie = cachedCookie
|
||||
}
|
||||
@ -48,7 +58,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
}
|
||||
}
|
||||
|
||||
fun play(radio: Radio) {
|
||||
fun play(radio: RadioEntity) {
|
||||
currentRadio = radio
|
||||
session = null
|
||||
|
||||
@ -70,6 +80,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
fun isActive() = currentRadio != null && session != null
|
||||
|
||||
private suspend fun createSession() {
|
||||
"createSession".log()
|
||||
|
||||
currentRadio?.let { radio ->
|
||||
try {
|
||||
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
|
||||
@ -83,18 +95,20 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
.authorize()
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
|
||||
.awaitObjectResponseResult<RadioSession>(AppContext.deserializer())
|
||||
|
||||
session = result.get().id
|
||||
cookie = response.header("set-cookie").joinToString(";")
|
||||
|
||||
Cache.set(context, "radio_type", radio.radio_type.toByteArray())
|
||||
radio.radio_type?.let { type -> Cache.set(context, "radio_type", type.toByteArray()) }
|
||||
Cache.set(context, "radio_id", radio.id.toString().toByteArray())
|
||||
Cache.set(context, "radio_session", session.toString().toByteArray())
|
||||
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
||||
|
||||
prepareNextTrack(true)
|
||||
} catch (e: Exception) {
|
||||
e.log()
|
||||
|
||||
withContext(Main) {
|
||||
context.toast(context.getString(R.string.radio_playback_error))
|
||||
}
|
||||
@ -103,6 +117,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
}
|
||||
|
||||
suspend fun prepareNextTrack(first: Boolean = false) {
|
||||
"prepareTrack".log()
|
||||
|
||||
session?.let { session ->
|
||||
try {
|
||||
val body = Gson().toJson(RadioTrackBody(session))
|
||||
@ -115,25 +131,23 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||
}
|
||||
}
|
||||
.body(body)
|
||||
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
|
||||
.awaitObjectResult<RadioTrack>(AppContext.deserializer())
|
||||
|
||||
val trackResponse = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
|
||||
val track = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
|
||||
.authorize()
|
||||
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
||||
.awaitObjectResult<FunkwhaleTrack>(AppContext.deserializer())
|
||||
.get()
|
||||
|
||||
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
Otter.get().database.tracks().run {
|
||||
insertWithAssocs(track)
|
||||
|
||||
val track = trackResponse.get().apply {
|
||||
favorite = favorites.contains(id)
|
||||
}
|
||||
|
||||
if (first) {
|
||||
CommandBus.send(Command.ReplaceQueue(listOf(track), true))
|
||||
} else {
|
||||
CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||
Track.fromDecoratedEntity(find(track.id)).let { track ->
|
||||
if (first) {
|
||||
CommandBus.send(Command.ReplaceQueue(listOf(track), true))
|
||||
} else {
|
||||
CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Main) {
|
||||
|
@ -1,32 +1,28 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.AlbumsCache
|
||||
import com.github.apognu.otter.utils.AlbumsResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||
import com.github.apognu.otter.models.dao.toDao
|
||||
|
||||
class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository<Album, AlbumsCache>() {
|
||||
override val cacheId: String by lazy {
|
||||
if (artistId == null) "albums"
|
||||
else "albums-artist-$artistId"
|
||||
}
|
||||
|
||||
override val upstream: Upstream<Album> by lazy {
|
||||
class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository<FunkwhaleAlbum>() {
|
||||
override val upstream: Upstream<FunkwhaleAlbum> by lazy {
|
||||
val url =
|
||||
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
|
||||
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
|
||||
|
||||
HttpUpstream<Album, OtterResponse<Album>>(
|
||||
HttpUpstream(
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
url,
|
||||
object : TypeToken<AlbumsResponse>() {}.type
|
||||
FunkwhaleAlbum.serializer()
|
||||
)
|
||||
}
|
||||
|
||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
|
||||
data.forEach {
|
||||
Otter.get().database.albums().insert(it.toDao())
|
||||
}
|
||||
|
||||
return super.onDataFetched(data)
|
||||
}
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.TracksCache
|
||||
import com.github.apognu.otter.utils.TracksResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-artist-$artistId"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<FunkwhaleTrack>() {
|
||||
override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", FunkwhaleTrack.serializer())
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhaleTrack>) = runBlocking(IO) {
|
||||
data.forEach {
|
||||
Otter.get().database.tracks().insertWithAssocs(it)
|
||||
}
|
||||
|
||||
super.onDataFetched(data)
|
||||
}
|
||||
}
|
@ -1,18 +1,33 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.ArtistsCache
|
||||
import com.github.apognu.otter.utils.ArtistsResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||
import com.github.apognu.otter.models.dao.toDao
|
||||
import com.github.apognu.otter.models.dao.toRealmDao
|
||||
import io.realm.Realm
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId = "artists"
|
||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
class ArtistsRepository(override val context: Context?) : Repository<FunkwhaleArtist>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
|
||||
scope.launch(IO) {
|
||||
data.forEach { artist ->
|
||||
Otter.get().database.artists().insert(artist.toDao())
|
||||
|
||||
Realm.getDefaultInstance().executeTransaction { realm ->
|
||||
realm.insertOrUpdate(artist.toRealmDao())
|
||||
}
|
||||
|
||||
artist.albums?.forEach { album ->
|
||||
Otter.get().database.albums().insert(album.toDao(artist.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onDataFetched(data)
|
||||
}
|
||||
}
|
@ -1,32 +1,33 @@
|
||||
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.apognu.otter.models.api.Favorited
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.models.dao.FavoriteEntity
|
||||
import com.github.apognu.otter.utils.Settings
|
||||
import com.github.apognu.otter.utils.mustNormalizeUrl
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
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.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedReader
|
||||
|
||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "favorites.v2"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
|
||||
class FavoritesRepository(override val context: Context?) : Repository<FunkwhaleTrack>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", FunkwhaleTrack.serializer())
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
val favoritedRepository = FavoritedRepository(context)
|
||||
|
||||
private val favoritedRepository = FavoritedRepository(context)
|
||||
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
||||
data.forEach {
|
||||
Otter.get().database.tracks().insertWithAssocs(it)
|
||||
Otter.get().database.favorites().insert(FavoriteEntity(it.id))
|
||||
}
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
|
||||
data.map { track ->
|
||||
track.favorite = true
|
||||
@ -39,10 +40,14 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||
}
|
||||
|
||||
track
|
||||
}
|
||||
} */
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
fun addFavorite(id: Int) {
|
||||
fun addFavorite(id: Int) = scope.launch(IO) {
|
||||
Otter.get().database.favorites().add(id)
|
||||
|
||||
val body = mapOf("track" to id)
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
|
||||
@ -57,11 +62,13 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
|
||||
favoritedRepository.update(context, scope)
|
||||
favoritedRepository.update()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFavorite(id: Int) {
|
||||
fun deleteFavorite(id: Int) = scope.launch(IO) {
|
||||
Otter.get().database.favorites().remove(id)
|
||||
|
||||
val body = mapOf("track" to id)
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
|
||||
@ -76,21 +83,26 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
|
||||
favoritedRepository.update(context, scope)
|
||||
favoritedRepository.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||
override val cacheId = "favorited"
|
||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Favorited>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer())
|
||||
|
||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
||||
|
||||
fun update(context: Context?, scope: CoroutineScope) {
|
||||
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
|
||||
Cache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
|
||||
override fun onDataFetched(data: List<Favorited>): List<Favorited> {
|
||||
scope.launch(IO) {
|
||||
data.forEach {
|
||||
Otter.get().database.favorites().insert(FavoriteEntity(it.track))
|
||||
}
|
||||
}
|
||||
|
||||
return super.onDataFetched(data)
|
||||
}
|
||||
|
||||
fun update() = scope.launch(IO) {
|
||||
fetch().collect()
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,29 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.models.api.OtterResponse
|
||||
import com.github.apognu.otter.models.api.OtterResponseSerializer
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.core.FuelError
|
||||
import com.github.kittinunf.fuel.core.ResponseDeserializable
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||
import com.github.kittinunf.result.Result
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.io.Reader
|
||||
import java.lang.reflect.Type
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlin.math.ceil
|
||||
|
||||
class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, private val serializer: KSerializer<D>) : Upstream<D> {
|
||||
enum class Behavior {
|
||||
Single, AtOnce, Progressive
|
||||
}
|
||||
|
||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
|
||||
if (behavior == Behavior.Single && size != 0) return@flow
|
||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = channelFlow {
|
||||
if (behavior == Behavior.Single && size != 0) return@channelFlow
|
||||
|
||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||
|
||||
@ -39,35 +38,32 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
||||
|
||||
get(url).fold(
|
||||
{ response ->
|
||||
val data = response.getData()
|
||||
val data = response.results
|
||||
|
||||
when (behavior) {
|
||||
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
|
||||
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
Behavior.Single -> send(Repository.Response(data, page, false))
|
||||
Behavior.Progressive -> send(Repository.Response(data, page, response.next != null))
|
||||
|
||||
else -> {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
send(Repository.Response(data, page, response.next != null))
|
||||
|
||||
if (response.next != null) fetch(size + data.size).collect { emit(it) }
|
||||
if (response.next != null) fetch(size + data.size).collect { send(it) }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
"GET $url".log()
|
||||
error.log()
|
||||
|
||||
when (error.exception) {
|
||||
is RefreshError -> EventBus.send(Event.LogOut)
|
||||
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
|
||||
else -> send(Repository.Response(listOf(), page, false))
|
||||
}
|
||||
}
|
||||
)
|
||||
}.flowOn(IO)
|
||||
|
||||
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
|
||||
override fun deserialize(reader: Reader): T? {
|
||||
return Gson().fromJson(reader, type)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(url: String): Result<R, FuelError> {
|
||||
suspend fun get(url: String): Result<OtterResponse<D>, FuelError> {
|
||||
return try {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
@ -75,7 +71,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
||||
}
|
||||
}
|
||||
|
||||
val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
|
||||
val (_, response, result) = request.awaitObjectResponseResult(AppContext.deserializer(OtterResponseSerializer(serializer)))
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
return retryGet(url)
|
||||
@ -87,7 +83,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun retryGet(url: String): Result<R, FuelError> {
|
||||
private suspend fun retryGet(url: String): Result<OtterResponse<D>, FuelError> {
|
||||
return try {
|
||||
return if (HTTP.refresh()) {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
@ -96,7 +92,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
|
||||
}
|
||||
}
|
||||
|
||||
request.awaitObjectResult(GenericDeserializer(type))
|
||||
request.awaitObjectResult(AppContext.deserializer(OtterResponseSerializer(serializer)))
|
||||
} else {
|
||||
Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
|
@ -1,33 +1,24 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.PlaylistTrack
|
||||
import com.github.apognu.otter.utils.PlaylistTracksCache
|
||||
import com.github.apognu.otter.utils.PlaylistTracksResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack
|
||||
import com.github.apognu.otter.models.dao.PlaylistTrack
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedReader
|
||||
|
||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||
override val cacheId = "tracks-playlist-$playlistId"
|
||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
class PlaylistTracksRepository(override val context: Context?, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer())
|
||||
|
||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>): List<FunkwhalePlaylistTrack> = runBlocking {
|
||||
Otter.get().database.playlists().replaceTracks(playlistId, data.map {
|
||||
Otter.get().database.tracks().insertWithAssocs(it.track)
|
||||
|
||||
override fun onDataFetched(data: List<PlaylistTrack>): List<PlaylistTrack> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
PlaylistTrack(playlistId, it.track.id)
|
||||
})
|
||||
|
||||
data.map { track ->
|
||||
track.track.favorite = favorites.contains(track.track.id)
|
||||
track
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
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.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhalePlaylist
|
||||
import com.github.apognu.otter.models.dao.toDao
|
||||
|
||||
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)
|
||||
class PlaylistsRepository(override val context: Context?) : Repository<FunkwhalePlaylist>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer())
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhalePlaylist>): List<FunkwhalePlaylist> {
|
||||
data.forEach {
|
||||
Otter.get().database.playlists().insert(it.toDao())
|
||||
}
|
||||
|
||||
return super.onDataFetched(data)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class QueueRepository(val scope: CoroutineScope) {
|
||||
fun all() = Otter.get().database.queue().allDecorated()
|
||||
|
||||
fun allBlocking() = Otter.get().database.queue().allDecoratedBlocking()
|
||||
|
||||
fun replace(tracks: List<Track>) = scope.launch {
|
||||
Otter.get().database.queue().replace(tracks)
|
||||
}
|
||||
}
|
@ -1,22 +1,19 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.RadiosCache
|
||||
import com.github.apognu.otter.utils.RadiosResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
||||
import com.github.apognu.otter.models.dao.toDao
|
||||
|
||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||
override val cacheId = "radios"
|
||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
|
||||
class RadiosRepository(override val context: Context?) : Repository<FunkwhaleRadio>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer())
|
||||
|
||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)
|
||||
override fun onDataFetched(data: List<FunkwhaleRadio>): List<FunkwhaleRadio> {
|
||||
data.forEach {
|
||||
Otter.get().database.radios().insert(it.toDao())
|
||||
}
|
||||
|
||||
override fun onDataFetched(data: List<Radio>): List<Radio> {
|
||||
return data
|
||||
.map { radio -> radio.apply { radio_type = "custom" } }
|
||||
.toMutableList()
|
||||
|
@ -1,59 +1,31 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.CacheItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.BufferedReader
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface Upstream<D> {
|
||||
fun fetch(size: Int = 0): Flow<Repository.Response<D>>
|
||||
}
|
||||
|
||||
abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||
abstract class Repository<D : Any> {
|
||||
protected val scope: CoroutineScope = CoroutineScope(Job() + IO)
|
||||
|
||||
enum class Origin(val origin: Int) {
|
||||
Cache(0b01),
|
||||
Network(0b10)
|
||||
}
|
||||
|
||||
data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)
|
||||
data class Response<D>(val data: List<D>, val page: Int, val hasMore: Boolean)
|
||||
|
||||
abstract val context: Context?
|
||||
abstract val cacheId: String?
|
||||
abstract val upstream: Upstream<D>
|
||||
|
||||
open fun cache(data: List<D>): C? = null
|
||||
protected open fun uncache(reader: BufferedReader): C? = null
|
||||
|
||||
fun fetch(upstreams: Int = Origin.Cache.origin and Origin.Network.origin, size: Int = 0): Flow<Response<D>> = flow {
|
||||
if (Origin.Cache.origin and upstreams == upstreams) fromCache().collect { emit(it) }
|
||||
if (Origin.Network.origin and upstreams == upstreams) fromNetwork(size).collect { emit(it) }
|
||||
}
|
||||
|
||||
private fun fromCache() = flow {
|
||||
cacheId?.let { cacheId ->
|
||||
Cache.get(context, cacheId)?.let { reader ->
|
||||
uncache(reader)?.let { cache ->
|
||||
return@flow emit(Response(Origin.Cache, cache.data, ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(), false))
|
||||
}
|
||||
}
|
||||
|
||||
return@flow emit(Response(Origin.Cache, listOf(), 1, false))
|
||||
}
|
||||
}.flowOn(IO)
|
||||
|
||||
private fun fromNetwork(size: Int) = flow {
|
||||
fun fetch(size: Int = 0) = channelFlow {
|
||||
upstream
|
||||
.fetch(size)
|
||||
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
|
||||
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
|
||||
.map { response -> Response(onDataFetched(response.data), response.page, response.hasMore) }
|
||||
.collect { response -> send(Response(response.data, response.page, response.hasMore)) }
|
||||
}
|
||||
|
||||
protected open fun onDataFetched(data: List<D>) = data
|
||||
|
@ -1,30 +1,24 @@
|
||||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedReader
|
||||
|
||||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Track>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleTrack>() {
|
||||
override val upstream: Upstream<FunkwhaleTrack>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
||||
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch()
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
|
||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
|
||||
data.map { track ->
|
||||
track.favorite = favorites.contains(track.id)
|
||||
@ -37,24 +31,18 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
||||
}
|
||||
|
||||
track
|
||||
}
|
||||
} */
|
||||
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Artist>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleArtist>() {
|
||||
override val upstream: Upstream<FunkwhaleArtist>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer())
|
||||
}
|
||||
|
||||
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Album>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
|
||||
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleAlbum>() {
|
||||
override val upstream: Upstream<FunkwhaleAlbum>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", FunkwhaleAlbum.serializer())
|
||||
}
|
@ -2,21 +2,14 @@ package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||
import com.github.apognu.otter.utils.getMetadata
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedReader
|
||||
|
||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-album-$albumId"
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<FunkwhaleTrack>() {
|
||||
override val upstream =
|
||||
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer())
|
||||
|
||||
companion object {
|
||||
fun getDownloadedIds(): List<Int>? {
|
||||
@ -37,25 +30,11 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
||||
data.forEach { track ->
|
||||
Otter.get().database.tracks().insertWithAssocs(track)
|
||||
}
|
||||
|
||||
val downloaded = getDownloadedIds() ?: listOf()
|
||||
|
||||
data.map { track ->
|
||||
track.favorite = favorites.contains(track.id)
|
||||
track.downloaded = downloaded.contains(track.id)
|
||||
|
||||
track.bestUpload()?.let { upload ->
|
||||
val url = mustNormalizeUrl(upload.listen_url)
|
||||
|
||||
track.cached = Otter.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||
}
|
||||
|
||||
track
|
||||
}.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
||||
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
||||
}
|
||||
}
|
@ -11,6 +11,12 @@ import android.os.Build
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.kittinunf.fuel.core.FuelManager
|
||||
import com.github.kittinunf.fuel.core.Method
|
||||
import com.github.kittinunf.fuel.core.ResponseDeserializable
|
||||
import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
object AppContext {
|
||||
const val PREFS_CREDENTIALS = "credentials"
|
||||
@ -23,6 +29,12 @@ object AppContext {
|
||||
const val PAGE_SIZE = 50
|
||||
const val TRANSITION_DURATION = 300L
|
||||
|
||||
inline fun <reified T : Any> deserializer(serializer: DeserializationStrategy<T>): ResponseDeserializable<T> =
|
||||
kotlinxDeserializerOf(loader = serializer, json = Json(JsonConfiguration(ignoreUnknownKeys = true)))
|
||||
|
||||
inline fun <reified T : Any> deserializer() =
|
||||
kotlinxDeserializerOf(T::class.serializer(), Json(JsonConfiguration(ignoreUnknownKeys = true)))
|
||||
|
||||
fun init(context: Activity) {
|
||||
setupNotificationChannels(context)
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
package com.github.apognu.otter.utils
|
||||
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Command {
|
||||
@ -28,7 +27,7 @@ sealed class Command {
|
||||
class MoveFromQueue(val oldPosition: Int, val newPosition: Int) : Command()
|
||||
object ClearQueue : Command()
|
||||
object ShuffleQueue : Command()
|
||||
class PlayRadio(val radio: Radio) : Command()
|
||||
class PlayRadio(val radio: RadioEntity) : Command()
|
||||
|
||||
class SetRepeatMode(val mode: Int) : Command()
|
||||
|
||||
@ -44,29 +43,12 @@ sealed class Event {
|
||||
|
||||
class PlaybackError(val message: String) : Event()
|
||||
object PlaybackStopped : Event()
|
||||
class Buffering(val value: Boolean) : Event()
|
||||
class TrackFinished(val track: Track?) : Event()
|
||||
class StateChanged(val playing: Boolean) : Event()
|
||||
object QueueChanged : Event()
|
||||
object RadioStarted : Event()
|
||||
object ListingsChanged : Event()
|
||||
class DownloadChanged(val download: Download) : Event()
|
||||
}
|
||||
|
||||
sealed class Request(var channel: Channel<Response>? = null) {
|
||||
object GetState : Request()
|
||||
object GetQueue : Request()
|
||||
object GetCurrentTrack : Request()
|
||||
object GetDownloads : Request()
|
||||
}
|
||||
|
||||
sealed class Response {
|
||||
class State(val playing: Boolean) : Response()
|
||||
class Queue(val queue: List<Track>) : Response()
|
||||
class CurrentTrack(val track: Track?) : Response()
|
||||
class Downloads(val cursor: DownloadCursor) : Response()
|
||||
}
|
||||
|
||||
object EventBus {
|
||||
fun send(event: Event) {
|
||||
GlobalScope.launch(IO) {
|
||||
@ -87,33 +69,3 @@ object CommandBus {
|
||||
fun get() = Otter.get().commandBus.asFlow()
|
||||
}
|
||||
|
||||
object RequestBus {
|
||||
fun send(request: Request): Channel<Response> {
|
||||
return Channel<Response>().also {
|
||||
GlobalScope.launch(IO) {
|
||||
request.channel = it
|
||||
|
||||
Otter.get().requestBus.offer(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get() = Otter.get().requestBus.asFlow()
|
||||
}
|
||||
|
||||
object ProgressBus {
|
||||
fun send(current: Int, duration: Int, percent: Int) {
|
||||
GlobalScope.launch(IO) {
|
||||
Otter.get().progressBus.send(Triple(current, duration, percent))
|
||||
}
|
||||
}
|
||||
|
||||
fun get() = Otter.get().progressBus.asFlow().conflate()
|
||||
}
|
||||
|
||||
suspend inline fun <reified T> Channel<Response>.wait(): T? {
|
||||
return when (val response = this.receive()) {
|
||||
is T -> response
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
package com.github.apognu.otter.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.activities.FwCredentials
|
||||
import com.github.apognu.otter.models.api.Credentials
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.core.FuelError
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.kittinunf.result.Result
|
||||
import com.preference.PowerPreference
|
||||
import java.io.BufferedReader
|
||||
@ -23,7 +22,9 @@ object HTTP {
|
||||
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("password")
|
||||
).toList()
|
||||
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/token"), body).awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
val result = Fuel
|
||||
.post(mustNormalizeUrl("/api/v1/token"), body)
|
||||
.awaitObjectResult<Credentials>(AppContext.deserializer())
|
||||
|
||||
return result.fold(
|
||||
{ data ->
|
||||
@ -42,7 +43,7 @@ object HTTP {
|
||||
}
|
||||
}
|
||||
|
||||
val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
|
||||
val (_, response, result) = request.awaitObjectResponseResult<T>(AppContext.deserializer())
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
return retryGet(url)
|
||||
@ -59,7 +60,7 @@ object HTTP {
|
||||
}
|
||||
}
|
||||
|
||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
||||
request.awaitObjectResult(AppContext.deserializer())
|
||||
} else {
|
||||
Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.os.Build
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.BrowseFragment
|
||||
import com.github.apognu.otter.models.api.DownloadInfo
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.kittinunf.fuel.core.Request
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
@ -17,10 +18,10 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
|
||||
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, page: Int, hasMore: Boolean) -> Unit) {
|
||||
scope.launch(context) {
|
||||
collect { data ->
|
||||
callback(data.data, data.origin == Repository.Origin.Cache, data.page, data.hasMore)
|
||||
callback(data.data, data.page, data.hasMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package com.github.apognu.otter.utils
|
||||
|
||||
import com.github.apognu.otter.models.api.User
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.github.kittinunf.result.Result
|
||||
import com.preference.PowerPreference
|
||||
|
||||
@ -12,7 +12,7 @@ object Userinfo {
|
||||
val hostname = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
||||
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
||||
.authorize()
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
|
||||
.awaitObjectResponseResult<User>(AppContext.deserializer())
|
||||
|
||||
return when (result) {
|
||||
is Result.Success -> {
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.models.domain.Upload
|
||||
|
||||
class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() {
|
||||
val albums: LiveData<List<Album>> by lazy {
|
||||
if (artistId == null) {
|
||||
Transformations.map(Otter.get().database.albums().allDecorated()) {
|
||||
it.map { album -> Album.fromDecoratedEntity(album) }
|
||||
}
|
||||
} else {
|
||||
Transformations.map(Otter.get().database.albums().forArtistDecorated(artistId)) {
|
||||
it.map { album -> Album.fromDecoratedEntity(album) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tracks(): List<Track> {
|
||||
artistId?.let {
|
||||
val tracks = Otter.get().database.tracks().ofArtistBlocking(artistId)
|
||||
val uploads = Otter.get().database.uploads().findAllBlocking(tracks.map { it.id })
|
||||
|
||||
return tracks.map {
|
||||
Track.fromDecoratedEntity(it).apply {
|
||||
this.uploads = uploads.filter { it.track_id == id }.map { Upload.fromEntity(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumViewModel(private val id: Int) : ViewModel() {
|
||||
val album: LiveData<Album> by lazy {
|
||||
Transformations.map(Otter.get().database.albums().getDecorated(id)) { album ->
|
||||
Album.fromDecoratedEntity(album)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations.map
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Artist
|
||||
|
||||
class ArtistsViewModel : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: ArtistsViewModel
|
||||
|
||||
fun get(): ArtistsViewModel {
|
||||
instance = if (::instance.isInitialized) instance else ArtistsViewModel()
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
val artists: LiveData<List<Artist>> = Otter.get().database.artists().allDecorated().map {
|
||||
it.map { Artist.fromDecoratedEntity(it) }
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistViewModel(private val id: Int) : ViewModel() {
|
||||
val artist: LiveData<Artist> by lazy {
|
||||
map(Otter.get().database.artists().getDecorated(id)) { artist ->
|
||||
Artist.fromDecoratedEntity(artist)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.models.api.DownloadInfo
|
||||
import com.github.apognu.otter.utils.getMetadata
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor
|
||||
|
||||
class DownloadsViewModel : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: DownloadsViewModel
|
||||
|
||||
fun get(): DownloadsViewModel {
|
||||
instance = if (::instance.isInitialized) instance else DownloadsViewModel()
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
val cursor: MutableLiveData<DownloadCursor> by lazy { MutableLiveData<DownloadCursor>() }
|
||||
val downloads: LiveData<List<DownloadInfo>> = Transformations.map(cursor) { cursor ->
|
||||
val downloads = mutableListOf<DownloadInfo>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val download = cursor.download
|
||||
|
||||
download.getMetadata()?.let { info ->
|
||||
downloads.add(info.apply {
|
||||
this.download = download
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
downloads.sortedBy { it.title }
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Album
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.models.domain.Upload
|
||||
|
||||
class FavoritesViewModel : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: FavoritesViewModel
|
||||
|
||||
fun get(): FavoritesViewModel {
|
||||
instance = if (::instance.isInitialized) instance else FavoritesViewModel()
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
private val _albums: LiveData<List<Album>> by lazy {
|
||||
Transformations.switchMap(_favorites) { tracks ->
|
||||
val ids = tracks.mapNotNull { it.album?.id }
|
||||
|
||||
Transformations.map(Otter.get().database.albums().findAllDecorated(ids)) { albums ->
|
||||
albums.map { album -> Album.fromDecoratedEntity(album) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _favorites: LiveData<List<Track>> by lazy {
|
||||
Transformations.switchMap(Otter.get().database.favorites().all()) {
|
||||
val ids = it.map { favorite -> favorite.track_id }
|
||||
|
||||
Transformations.map(Otter.get().database.tracks().findAllDecorated(ids)) { tracks ->
|
||||
tracks.map { track -> Track.fromDecoratedEntity(track) }.sortedBy { it.title }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _uploads: LiveData<List<Upload>> by lazy {
|
||||
Transformations.switchMap(_favorites) { tracks ->
|
||||
val ids = tracks.mapNotNull { it.album?.id }
|
||||
|
||||
Transformations.map(Otter.get().database.uploads().findAll(ids)) { uploads ->
|
||||
uploads.map { upload -> Upload.fromEntity(upload) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val favorites = MediatorLiveData<List<Track>>().apply {
|
||||
addSource(_favorites) { merge(_favorites, _albums, _uploads) }
|
||||
addSource(_albums) { merge(_favorites, _albums, _uploads) }
|
||||
addSource(_uploads) { merge(_favorites, _albums, _uploads) }
|
||||
}
|
||||
|
||||
private fun merge(_tracks: LiveData<List<Track>>, _albums: LiveData<List<Album>>, _uploads: LiveData<List<Upload>>) {
|
||||
val _tracks = _tracks.value
|
||||
val _albums = _albums.value
|
||||
val _uploads = _uploads.value
|
||||
|
||||
if (_tracks == null || _albums == null || _uploads == null) {
|
||||
return
|
||||
}
|
||||
|
||||
favorites.value = _tracks.map { track ->
|
||||
track.uploads = _uploads.filter { upload -> upload.track_id == track.id }
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
|
||||
class PlayerStateViewModel private constructor() : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: PlayerStateViewModel
|
||||
|
||||
fun get(): PlayerStateViewModel {
|
||||
instance = if (::instance.isInitialized) instance else PlayerStateViewModel()
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
val isPlaying: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
|
||||
val isBuffering: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
|
||||
val position: MutableLiveData<Triple<Int, Int, Int>> by lazy { MutableLiveData<Triple<Int, Int, Int>>() }
|
||||
|
||||
val _track: MutableLiveData<Track> by lazy { MutableLiveData<Track>() }
|
||||
|
||||
val track: LiveData<Track> by lazy {
|
||||
Transformations.switchMap(_track) {
|
||||
if (it == null) {
|
||||
return@switchMap null
|
||||
}
|
||||
|
||||
Otter.get().database.tracks().getDecorated(it.id).map { Track.fromDecoratedEntity(it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
|
||||
class PlaylistsViewModel : ViewModel() {
|
||||
val playlists: LiveData<List<PlaylistEntity>> by lazy { Otter.get().database.playlists().all() }
|
||||
}
|
||||
|
||||
class PlaylistViewModel(playlistId: Int) : ViewModel() {
|
||||
val tracks: LiveData<List<Track>> by lazy {
|
||||
Transformations.map(Otter.get().database.playlists().tracksFor(playlistId)) {
|
||||
it.map { track -> Track.fromDecoratedEntity(track) }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.repositories.QueueRepository
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class QueueViewModel private constructor() : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: QueueViewModel
|
||||
|
||||
fun get(): QueueViewModel {
|
||||
instance = if (::instance.isInitialized) instance else QueueViewModel()
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
private val queueRepository = QueueRepository(viewModelScope)
|
||||
|
||||
private val _cached = liveData {
|
||||
while (true) {
|
||||
emit(Otter.get().exoCache.keys)
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
private val _current = PlayerStateViewModel.get().track
|
||||
|
||||
private val _queue: LiveData<List<Track>> by lazy {
|
||||
Transformations.map(queueRepository.all()) { tracks ->
|
||||
tracks.map { Track.fromDecoratedEntity(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val queue = MediatorLiveData<List<Track>>().apply {
|
||||
addSource(_queue) { merge(_queue, _current, _cached) }
|
||||
addSource(_current) { merge(_queue, _current, _cached) }
|
||||
addSource(_cached) { merge(_queue, _current, _cached) }
|
||||
}
|
||||
|
||||
private fun merge(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>) {
|
||||
val _tracks = _tracks.value
|
||||
val _current = _current.value
|
||||
val _cached = _cached.value
|
||||
|
||||
if (_tracks == null || _cached == null) {
|
||||
return
|
||||
}
|
||||
|
||||
queue.value = _tracks.map { track ->
|
||||
track.current = _current?.id == track.id
|
||||
track.cached = _cached.contains(maybeNormalizeUrl(track.bestUpload()?.listen_url))
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.dao.RadioEntity
|
||||
|
||||
class RadiosViewModel : ViewModel() {
|
||||
companion object {
|
||||
private lateinit var instance: RadiosViewModel
|
||||
|
||||
fun get(): RadiosViewModel {
|
||||
instance = if (::instance.isInitialized) instance else RadiosViewModel()
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
val radios: LiveData<List<RadioEntity>> by lazy { Otter.get().database.radios().all() }
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.models.domain.Track
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class TracksViewModel(private val albumId: Int) : ViewModel() {
|
||||
private val _cached = liveData {
|
||||
while (true) {
|
||||
emit(Otter.get().exoCache.keys)
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
private val _current = PlayerStateViewModel.get().track
|
||||
|
||||
private val _tracks: LiveData<List<Track>> by lazy {
|
||||
Transformations.map(Otter.get().database.tracks().ofAlbumsDecorated(listOf(albumId))) {
|
||||
it.map { track -> Track.fromDecoratedEntity(track) }
|
||||
}
|
||||
}
|
||||
|
||||
private val _favorites: LiveData<List<Track>> by lazy {
|
||||
Transformations.map(Otter.get().database.tracks().favorites()) {
|
||||
it.map { track -> Track.fromDecoratedEntity(track) }
|
||||
}
|
||||
}
|
||||
|
||||
val tracks = MediatorLiveData<List<Track>>().apply {
|
||||
addSource(_tracks) { mergeTracks(_tracks, _current, _cached) }
|
||||
addSource(_current) { mergeTracks(_tracks, _current, _cached) }
|
||||
addSource(_cached) { mergeTracks(_tracks, _current, _cached) }
|
||||
}
|
||||
|
||||
val favorites = MediatorLiveData<List<Track>>().apply {
|
||||
addSource(_favorites) { mergeFavorites(_favorites, _current, _cached) }
|
||||
addSource(_current) { mergeFavorites(_favorites, _current, _cached) }
|
||||
addSource(_cached) { mergeFavorites(_favorites, _current, _cached) }
|
||||
}
|
||||
|
||||
private fun mergeTracks(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>) {
|
||||
tracks.value = merge(_tracks, _current, _cached) ?: return
|
||||
}
|
||||
|
||||
private fun mergeFavorites(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>) {
|
||||
favorites.value = merge(_tracks, _current, _cached) ?: return
|
||||
}
|
||||
|
||||
private fun merge(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>): List<Track>? {
|
||||
val _tracks = _tracks.value
|
||||
val _current = _current.value
|
||||
val _cached = _cached.value
|
||||
|
||||
if (_tracks == null || _cached == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return _tracks.map { track ->
|
||||
track.current = _current?.id == track.id
|
||||
track.cached = _cached.contains(maybeNormalizeUrl(track.bestUpload()?.listen_url))
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
<com.github.apognu.otter.views.DisableableFrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
@ -19,7 +19,7 @@
|
||||
android:layout_weight="1"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<FrameLayout
|
||||
<com.github.apognu.otter.views.DisableableFrameLayout
|
||||
android:id="@+id/landscape_queue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -41,6 +41,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -7,6 +7,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.0.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
|
||||
classpath("io.realm:realm-gradle-plugin:10.0.0-BETA.6")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
org.gradle.jvmargs=-Xmx4608m
|
||||
|
||||
kotlin.code.style=official
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user