WIP - Integrate Room and LiveData.
This commit is contained in:
parent
e60814d28f
commit
567a7476f9
|
@ -6,10 +6,14 @@ plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-android-extensions")
|
id("kotlin-android-extensions")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("realm-android")
|
||||||
|
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
|
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
|
||||||
id("com.gladed.androidgitversion") version "0.4.10"
|
id("com.gladed.androidgitversion") version "0.4.10"
|
||||||
id("com.github.triplet.play") version "2.4.2"
|
id("com.github.triplet.play") version "2.4.2"
|
||||||
|
|
||||||
|
kotlin("plugin.serialization") version "1.3.70"
|
||||||
}
|
}
|
||||||
|
|
||||||
val props = Properties().apply {
|
val props = Properties().apply {
|
||||||
|
@ -63,9 +67,9 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
applicationIdSuffix = ".dev"
|
applicationIdSuffix = ".dev.livedata"
|
||||||
manifestPlaceholders = mapOf(
|
manifestPlaceholders = mapOf(
|
||||||
"app_name" to "Otter (develop)"
|
"app_name" to "Otter (livedata)"
|
||||||
)
|
)
|
||||||
|
|
||||||
resValue("string", "debug.hostname", props.getProperty("debug.hostname", ""))
|
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.kotlin:kotlin-stdlib-jdk7:1.3.72")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7")
|
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-coroutines-android:1.3.7")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0")
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.2.0")
|
implementation("androidx.appcompat:appcompat:1.2.0")
|
||||||
implementation("androidx.core:core-ktx:1.5.0-alpha02")
|
implementation("androidx.core:core-ktx:1.5.0-alpha02")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
|
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.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.preference:preference:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||||
|
@ -134,11 +143,21 @@ dependencies {
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
|
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
|
||||||
|
|
||||||
implementation("com.aliassadi:power-preference-lib:1.4.1")
|
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-coroutines:2.1.0")
|
||||||
implementation("com.github.kittinunf.fuel:fuel-android: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-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.google.code.gson:gson:2.8.6")
|
||||||
implementation("com.squareup.picasso:picasso:2.71828")
|
implementation("com.squareup.picasso:picasso:2.71828")
|
||||||
implementation("jp.wasabeef:picasso-transformations:2.2.1")
|
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 android.app.Application
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
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.MediaSession
|
||||||
import com.github.apognu.otter.playback.QueueManager
|
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.database.ExoDatabaseProvider
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
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.NoOpCacheEvictor
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
|
import io.realm.Realm
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.channels.BroadcastChannel
|
||||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -30,8 +35,12 @@ class Otter : Application() {
|
||||||
|
|
||||||
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
|
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
|
||||||
val commandBus: BroadcastChannel<Command> = 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) }
|
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
|
||||||
|
|
||||||
|
@ -66,6 +75,8 @@ class Otter : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
Realm.init(this)
|
||||||
|
|
||||||
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
||||||
|
|
|
@ -3,13 +3,16 @@ package com.github.apognu.otter.activities
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.DownloadsAdapter
|
import com.github.apognu.otter.adapters.DownloadsAdapter
|
||||||
import com.github.apognu.otter.utils.Event
|
import com.github.apognu.otter.utils.Event
|
||||||
import com.github.apognu.otter.utils.EventBus
|
import com.github.apognu.otter.utils.EventBus
|
||||||
import com.github.apognu.otter.utils.getMetadata
|
import com.github.apognu.otter.utils.getMetadata
|
||||||
|
import com.github.apognu.otter.viewmodels.DownloadsViewModel
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import kotlinx.android.synthetic.main.activity_downloads.*
|
import kotlinx.android.synthetic.main.activity_downloads.*
|
||||||
import kotlinx.coroutines.Dispatchers.Default
|
import kotlinx.coroutines.Dispatchers.Default
|
||||||
|
@ -27,15 +30,20 @@ class DownloadsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
setContentView(R.layout.activity_downloads)
|
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)
|
it.setHasStableIds(true)
|
||||||
|
|
||||||
downloads.layoutManager = LinearLayoutManager(this)
|
downloads.layoutManager = LinearLayoutManager(this)
|
||||||
downloads.adapter = it
|
downloads.adapter = it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DownloadsViewModel.get().downloads.observe(this) { downloads ->
|
||||||
|
adapter.downloads = downloads.toMutableList()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(Default) {
|
lifecycleScope.launch(Default) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1000)
|
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) {
|
private suspend fun refreshTrack(download: Download) {
|
||||||
download.getMetadata()?.let { info ->
|
download.getMetadata()?.let { info ->
|
||||||
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
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) {
|
if (download.state != info.download?.state) {
|
||||||
withContext(Main) {
|
adapter.downloads.getOrNull(match.second)?.let {
|
||||||
adapter.downloads[match.second] = info.apply {
|
withContext(Main) {
|
||||||
this.download = download
|
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 androidx.lifecycle.lifecycleScope
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.LoginDialog
|
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.AppContext
|
||||||
import com.github.apognu.otter.utils.Userinfo
|
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.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.android.synthetic.main.activity_login.*
|
import kotlinx.android.synthetic.main.activity_login.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
|
|
||||||
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
@ImplicitReflectionSerializer
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -102,11 +103,13 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
try {
|
try {
|
||||||
val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body)
|
val (_, response, result) =
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
Fuel
|
||||||
|
.post("$hostname/api/v1/token/", body)
|
||||||
|
.awaitObjectResponseResult<Credentials>(AppContext.deserializer())
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success<*> -> {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||||
setString("hostname", hostname)
|
setString("hostname", hostname)
|
||||||
setBoolean("anonymous", false)
|
setBoolean("anonymous", false)
|
||||||
|
@ -128,7 +131,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
is Result.Failure -> {
|
is Result.Failure -> {
|
||||||
dialog.dismiss()
|
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
|
hostname_field.error = null
|
||||||
username_field.error = null
|
username_field.error = null
|
||||||
|
@ -160,7 +163,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
try {
|
try {
|
||||||
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
|
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
.awaitObjectResponseResult<Credentials>(AppContext.deserializer())
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.github.apognu.otter.activities
|
||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
@ -10,7 +9,6 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
@ -21,15 +19,19 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.*
|
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.MediaControlsManager
|
||||||
import com.github.apognu.otter.playback.PinService
|
import com.github.apognu.otter.playback.PinService
|
||||||
import com.github.apognu.otter.playback.PlayerService
|
import com.github.apognu.otter.playback.PlayerService
|
||||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
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.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.apognu.otter.views.DisableableFrameLayout
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
|
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.google.gson.Gson
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
|
import io.realm.Realm
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.partial_now_playing.*
|
import kotlinx.android.synthetic.main.partial_now_playing.*
|
||||||
|
@ -53,7 +56,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
LOGOUT(1001)
|
LOGOUT(1001)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val favoriteRepository = FavoritesRepository(this)
|
private val queueViewModel = QueueViewModel.get()
|
||||||
|
private val favoritesRepository = FavoritesRepository(this)
|
||||||
private val favoritedRepository = FavoritedRepository(this)
|
private val favoritedRepository = FavoritedRepository(this)
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
|
@ -75,12 +79,51 @@ class MainActivity : AppCompatActivity() {
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
watchEventBus()
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
|
container?.setShouldRegisterTouch { _ ->
|
||||||
if (now_playing.isOpened()) {
|
if (now_playing.isOpened()) {
|
||||||
now_playing.close()
|
now_playing.close()
|
||||||
|
|
||||||
|
@ -90,7 +133,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
true
|
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))
|
startService(Intent(this, PlayerService::class.java))
|
||||||
DownloadService.start(this, PinService::class.java)
|
DownloadService.start(this, PinService::class.java)
|
||||||
|
@ -257,6 +310,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
||||||
startActivity(this)
|
startActivity(this)
|
||||||
|
|
||||||
|
databaseList().forEach {
|
||||||
|
deleteDatabase(it)
|
||||||
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,13 +358,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
is Event.PlaybackError -> toast(message.message)
|
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 -> {
|
is Event.PlaybackStopped -> {
|
||||||
if (now_playing.visibility == View.VISIBLE) {
|
if (now_playing.visibility == View.VISIBLE) {
|
||||||
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
(container.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||||
|
@ -332,30 +383,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Event.TrackFinished -> incrementListenCount(message.track)
|
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?) {
|
private fun refreshCurrentTrack(track: Track?) {
|
||||||
|
@ -424,15 +433,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
now_playing_title.text = track.title
|
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_toggle.icon = getDrawable(R.drawable.pause)
|
||||||
|
|
||||||
now_playing_details_title.text = track.title
|
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)
|
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
.maybeLoad(maybeNormalizeUrl(track.album?.cover))
|
||||||
.fit()
|
.fit()
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(now_playing_cover)
|
.into(now_playing_cover)
|
||||||
|
@ -499,34 +508,22 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now_playing_details_favorite?.let { now_playing_details_favorite ->
|
when (track.favorite) {
|
||||||
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
|
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||||
lifecycleScope.launch(Main) {
|
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||||
track.favorite = favorites.contains(track.id)
|
}
|
||||||
|
|
||||||
when (track.favorite) {
|
now_playing_details_favorite.setOnClickListener {
|
||||||
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
when (track.favorite) {
|
||||||
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
true -> {
|
||||||
}
|
favoritesRepository.deleteFavorite(track.id)
|
||||||
}
|
// 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
track.favorite = !track.favorite
|
false -> {
|
||||||
|
favoritesRepository.addFavorite(track.id)
|
||||||
favoriteRepository.fetch(Repository.Origin.Network.origin)
|
// now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,24 @@ import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.SearchAdapter
|
import com.github.apognu.otter.adapters.SearchAdapter
|
||||||
import com.github.apognu.otter.fragments.AlbumsFragment
|
import com.github.apognu.otter.fragments.AlbumsFragment
|
||||||
import com.github.apognu.otter.fragments.ArtistsFragment
|
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.repositories.*
|
||||||
import com.github.apognu.otter.utils.*
|
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 com.google.android.exoplayer2.offline.Download
|
||||||
import kotlinx.android.synthetic.main.activity_search.*
|
import kotlinx.android.synthetic.main.activity_search.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@ class SearchActivity : AppCompatActivity() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(IO) {
|
||||||
EventBus.get().collect { message ->
|
EventBus.get().collect { message ->
|
||||||
when (message) {
|
when (message) {
|
||||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||||
|
@ -82,25 +88,52 @@ class SearchActivity : AppCompatActivity() {
|
||||||
adapter.tracks.clear()
|
adapter.tracks.clear()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|
||||||
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
|
artistsRepository.fetch().untilNetwork(lifecycleScope, IO) { artists, _, _ ->
|
||||||
done++
|
done++
|
||||||
|
|
||||||
adapter.artists.addAll(artists)
|
artists.forEach {
|
||||||
refresh()
|
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++
|
done++
|
||||||
|
|
||||||
adapter.albums.addAll(albums)
|
albums.forEach {
|
||||||
refresh()
|
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++
|
done++
|
||||||
|
|
||||||
adapter.tracks.addAll(tracks)
|
tracks.forEach {
|
||||||
refresh()
|
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) {
|
private suspend fun refreshDownloadedTrack(download: Download) {
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
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 ->
|
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter.tracks[match.second].downloaded = true
|
adapter.tracks[match.second].downloaded = true
|
||||||
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
|
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.Cache
|
||||||
import com.github.apognu.otter.utils.Command
|
import com.github.apognu.otter.utils.Command
|
||||||
import com.github.apognu.otter.utils.CommandBus
|
import com.github.apognu.otter.utils.CommandBus
|
||||||
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -35,6 +36,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
fun getThemeResId(): Int = R.style.AppTheme
|
fun getThemeResId(): Int = R.style.AppTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ImplicitReflectionSerializer
|
||||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
|
@ -7,9 +7,9 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.Album
|
|
||||||
import com.github.apognu.otter.utils.maybeLoad
|
import com.github.apognu.otter.utils.maybeLoad
|
||||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
|
import com.github.apognu.otter.models.domain.Album
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_album.view.*
|
import kotlinx.android.synthetic.main.row_album.view.*
|
||||||
|
@ -42,7 +42,7 @@ class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickLis
|
||||||
.into(holder.art)
|
.into(holder.art)
|
||||||
|
|
||||||
holder.title.text = album.title
|
holder.title.text = album.title
|
||||||
holder.artist.text = album.artist.name
|
holder.artist.text = album.artist_name
|
||||||
holder.release_date.visibility = View.GONE
|
holder.release_date.visibility = View.GONE
|
||||||
|
|
||||||
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
|
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
|
||||||
|
|
|
@ -7,9 +7,9 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.Album
|
|
||||||
import com.github.apognu.otter.utils.maybeLoad
|
import com.github.apognu.otter.utils.maybeLoad
|
||||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
|
import com.github.apognu.otter.models.domain.Album
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_album_grid.view.*
|
import kotlinx.android.synthetic.main.row_album_grid.view.*
|
||||||
|
|
|
@ -7,39 +7,21 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.Artist
|
|
||||||
import com.github.apognu.otter.utils.maybeLoad
|
import com.github.apognu.otter.utils.maybeLoad
|
||||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
|
import com.github.apognu.otter.models.domain.Artist
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_artist.view.*
|
import kotlinx.android.synthetic.main.row_artist.view.*
|
||||||
|
|
||||||
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
||||||
private var active: List<Artist> = mutableListOf()
|
|
||||||
|
|
||||||
interface OnArtistClickListener {
|
interface OnArtistClickListener {
|
||||||
fun onClick(holder: View?, artist: Artist)
|
fun onClick(holder: View?, artist: Artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
override fun getItemCount() = data.size
|
||||||
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
||||||
override fun onChanged() {
|
|
||||||
active = data.filter { it.albums?.isNotEmpty() ?: false }
|
|
||||||
|
|
||||||
super.onChanged()
|
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||||
}
|
|
||||||
|
|
||||||
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 onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
|
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) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val artist = active[position]
|
val artist = data[position]
|
||||||
|
|
||||||
artist.albums?.let { albums ->
|
Picasso.get()
|
||||||
if (albums.isNotEmpty()) {
|
.maybeLoad(maybeNormalizeUrl(artist.album_cover))
|
||||||
Picasso.get()
|
.fit()
|
||||||
.maybeLoad(maybeNormalizeUrl(albums[0].cover?.urls?.original))
|
.transform(RoundedCornersTransformation(8, 0))
|
||||||
.fit()
|
.into(holder.art)
|
||||||
.transform(RoundedCornersTransformation(8, 0))
|
|
||||||
.into(holder.art)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.name.text = artist.name
|
holder.name.text = artist.name
|
||||||
|
|
||||||
artist.albums?.let {
|
context?.let {
|
||||||
context?.let {
|
holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.album_count, artist.album_count)
|
||||||
holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.albums.size, artist.albums.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +53,9 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
||||||
val albums = view.albums
|
val albums = view.albums
|
||||||
|
|
||||||
override fun onClick(view: View?) {
|
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 android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
|
import com.github.apognu.otter.models.api.DownloadInfo
|
||||||
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
import com.github.apognu.otter.playback.PinService
|
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.Download
|
||||||
import com.google.android.exoplayer2.offline.DownloadService
|
import com.google.android.exoplayer2.offline.DownloadService
|
||||||
import kotlinx.android.synthetic.main.row_download.view.*
|
import kotlinx.android.synthetic.main.row_download.view.*
|
||||||
|
|
||||||
class DownloadsAdapter(private val context: Context, private val listener: OnDownloadChangedListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
|
class DownloadsAdapter(private val context: Context) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
|
||||||
interface OnDownloadChangedListener {
|
|
||||||
fun onItemRemoved(index: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloads: MutableList<DownloadInfo> = mutableListOf()
|
var downloads: MutableList<DownloadInfo> = mutableListOf()
|
||||||
|
|
||||||
override fun getItemCount() = downloads.size
|
|
||||||
|
|
||||||
override fun getItemId(position: Int) = downloads[position].id.toLong()
|
override fun getItemId(position: Int) = downloads[position].id.toLong()
|
||||||
|
|
||||||
|
override fun getItemCount() = downloads.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.row_download, parent, false)
|
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_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false)
|
||||||
|
|
||||||
Download.STATE_FAILED -> {
|
Download.STATE_FAILED -> {
|
||||||
Track.fromDownload(download).also {
|
FunkwhaleTrack.fromDownload(download).also {
|
||||||
PinService.download(context, it)
|
// PinService.download(context, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +86,6 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.delete.setOnClickListener {
|
holder.delete.setOnClickListener {
|
||||||
listener.onItemRemoved(position)
|
|
||||||
DownloadService.sendRemoveDownload(context, PinService::class.java, download.contentId, false)
|
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.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
|
import com.github.apognu.otter.models.domain.Track
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_track.view.*
|
import kotlinx.android.synthetic.main.row_track.view.*
|
||||||
|
@ -51,7 +52,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
|
||||||
.into(holder.cover)
|
.into(holder.cover)
|
||||||
|
|
||||||
holder.title.text = favorite.title
|
holder.title.text = favorite.title
|
||||||
holder.artist.text = favorite.artist.name
|
holder.artist.text = favorite.artist?.name
|
||||||
|
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
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.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
|
import com.github.apognu.otter.models.domain.Track
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_track.view.*
|
import kotlinx.android.synthetic.main.row_track.view.*
|
||||||
import java.util.*
|
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 {
|
interface OnFavoriteListener {
|
||||||
fun onToggleFavorite(id: Int, state: Boolean)
|
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 getItemCount() = data.size
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
return data[position].track.id.toLong()
|
return data[position].id.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
@ -56,38 +57,33 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||||
val track = data[position]
|
val track = data[position]
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
.maybeLoad(maybeNormalizeUrl(track.track.album?.cover()))
|
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
|
||||||
.fit()
|
.fit()
|
||||||
.placeholder(R.drawable.cover)
|
.placeholder(R.drawable.cover)
|
||||||
.transform(RoundedCornersTransformation(16, 0))
|
.transform(RoundedCornersTransformation(16, 0))
|
||||||
.into(holder.cover)
|
.into(holder.cover)
|
||||||
|
|
||||||
holder.title.text = track.track.title
|
holder.title.text = track.title
|
||||||
holder.artist.text = track.track.artist.name
|
holder.artist.text = track.artist?.name
|
||||||
|
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.track == currentTrack || track.track.current) {
|
if (track == currentTrack) {
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.itemView.background = context.getDrawable(R.drawable.current)
|
holder.itemView.background = context.getDrawable(R.drawable.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context?.let {
|
context?.let {
|
||||||
when (track.track.favorite) {
|
when (track.favorite) {
|
||||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||||
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.favorite.setOnClickListener {
|
holder.favorite.setOnClickListener {
|
||||||
favoriteListener?.let {
|
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||||
favoriteListener.onToggleFavorite(track.track.id, !track.track.favorite)
|
|
||||||
|
|
||||||
track.track.favorite = !track.track.favorite
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +94,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track)))
|
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track))
|
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
||||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.track))
|
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -152,8 +148,8 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
||||||
when (fromQueue) {
|
when (fromQueue) {
|
||||||
true -> CommandBus.send(Command.PlayTrack(layoutPosition))
|
true -> CommandBus.send(Command.PlayTrack(layoutPosition))
|
||||||
false -> {
|
false -> {
|
||||||
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
|
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).also { track ->
|
||||||
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
|
CommandBus.send(Command.ReplaceQueue(track))
|
||||||
|
|
||||||
context.toast("All tracks were added to your queue")
|
context.toast("All tracks were added to your queue")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,16 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
|
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.Playlist
|
|
||||||
import com.github.apognu.otter.utils.toDurationString
|
import com.github.apognu.otter.utils.toDurationString
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_playlist.view.*
|
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 {
|
interface OnPlaylistClickListener {
|
||||||
fun onClick(holder: View?, playlist: Playlist)
|
fun onClick(holder: View?, playlist: PlaylistEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = data.size
|
override fun getItemCount() = data.size
|
||||||
|
|
|
@ -6,11 +6,11 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
|
import com.github.apognu.otter.models.dao.RadioEntity
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
import com.github.apognu.otter.utils.AppContext
|
import com.github.apognu.otter.utils.AppContext
|
||||||
import com.github.apognu.otter.utils.Event
|
import com.github.apognu.otter.utils.Event
|
||||||
import com.github.apognu.otter.utils.EventBus
|
import com.github.apognu.otter.utils.EventBus
|
||||||
import com.github.apognu.otter.utils.Radio
|
|
||||||
import com.github.apognu.otter.views.LoadingImageView
|
import com.github.apognu.otter.views.LoadingImageView
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.android.synthetic.main.row_radio.view.*
|
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.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
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 {
|
interface OnRadioClickListener {
|
||||||
fun onClick(holder: ViewHolder, radio: Radio)
|
fun onClick(holder: ViewHolder, radio: RadioEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RowType {
|
enum class RowType {
|
||||||
|
@ -31,26 +31,26 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
|
||||||
UserRadio
|
UserRadio
|
||||||
}
|
}
|
||||||
|
|
||||||
private val instanceRadios: List<Radio> by lazy {
|
private val instanceRadios: List<RadioEntity> by lazy {
|
||||||
context?.let {
|
context?.let {
|
||||||
return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) {
|
return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) {
|
||||||
"" -> listOf(
|
"" -> 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(
|
else -> listOf(
|
||||||
Radio(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username),
|
RadioEntity(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)),
|
RadioEntity(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)),
|
RadioEntity(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, "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)) {
|
return when (getItemViewType(position)) {
|
||||||
RowType.InstanceRadio.ordinal -> instanceRadios[position - 1]
|
RowType.InstanceRadio.ordinal -> instanceRadios[position - 1]
|
||||||
else -> data[position - instanceRadios.size - 2]
|
else -> data[position - instanceRadios.size - 2]
|
||||||
|
|
|
@ -13,7 +13,11 @@ import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
import com.github.apognu.otter.utils.*
|
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 com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_track.view.*
|
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 albums: MutableList<Album> = mutableListOf()
|
||||||
var tracks: MutableList<Track> = 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
|
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) {
|
if (resultType == ResultType.Track.ordinal) {
|
||||||
(item as? Track)?.let { track ->
|
(item as? Track)?.let { track ->
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
if (track == currentTrack || track.current) {
|
/* if (track == currentTrack || track.current) {
|
||||||
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
|
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
|
||||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
|
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
|
||||||
}
|
} */
|
||||||
|
|
||||||
when (track.favorite) {
|
when (track.favorite) {
|
||||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
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 {
|
holder.favorite.setOnClickListener {
|
||||||
favoriteListener?.let {
|
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||||
favoriteListener.onToggleFavorite(track.id, !track.favorite)
|
|
||||||
|
|
||||||
tracks[position - artists.size - albums.size - SECTION_COUNT].favorite = !track.favorite
|
|
||||||
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (track.cached || track.downloaded) {
|
when (track.cached || track.downloaded) {
|
||||||
|
|
|
@ -2,7 +2,9 @@ package com.github.apognu.otter.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
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.graphics.drawable.ColorDrawable
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
@ -11,6 +13,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
|
import com.github.apognu.otter.models.domain.Track
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
|
@ -24,8 +27,6 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||||
|
|
||||||
private lateinit var touchHelper: ItemTouchHelper
|
private lateinit var touchHelper: ItemTouchHelper
|
||||||
|
|
||||||
var currentTrack: Track? = null
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
||||||
|
|
||||||
override fun getItemCount() = data.size
|
override fun getItemCount() = data.size
|
||||||
|
@ -59,32 +60,22 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||||
.into(holder.cover)
|
.into(holder.cover)
|
||||||
|
|
||||||
holder.title.text = track.title
|
holder.title.text = track.title
|
||||||
holder.artist.text = track.artist.name
|
holder.artist.text = track.artist?.name
|
||||||
|
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
|
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
|
||||||
}
|
|
||||||
|
|
||||||
if (track == currentTrack || track.current) {
|
if (track.current) {
|
||||||
context?.let {
|
|
||||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
|
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
context?.let {
|
|
||||||
when (track.favorite) {
|
when (track.favorite) {
|
||||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||||
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.favorite.setOnClickListener {
|
holder.favorite.setOnClickListener {
|
||||||
favoriteListener?.let {
|
favoriteListener?.onToggleFavorite(track.id, !track.favorite)
|
||||||
favoriteListener.onToggleFavorite(track.id, !track.favorite)
|
|
||||||
|
|
||||||
data[position].favorite = !track.favorite
|
|
||||||
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (track.cached || track.downloaded) {
|
when (track.cached || track.downloaded) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
|
@ -16,21 +17,21 @@ import androidx.transition.Slide
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.adapters.AlbumsAdapter
|
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.AlbumsRepository
|
||||||
import com.github.apognu.otter.repositories.ArtistTracksRepository
|
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.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 com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.fragment_albums.*
|
import kotlinx.android.synthetic.main.fragment_albums.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
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.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 viewRes = R.layout.fragment_albums
|
||||||
override val recycler: RecyclerView get() = albums
|
override val recycler: RecyclerView get() = albums
|
||||||
override val alwaysRefresh = false
|
override val alwaysRefresh = false
|
||||||
|
@ -92,14 +93,16 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
artistId = getInt("artistId")
|
artistId = getInt("artistId")
|
||||||
artistName = getString("artistName") ?: ""
|
artistName = getString("artistName") ?: ""
|
||||||
artistArt = getString("artistArt") ?: ""
|
artistArt = getString("artistArt") ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveData = AlbumsViewModel(artistId).albums
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
adapter = AlbumsAdapter(context, OnAlbumClickListener())
|
adapter = AlbumsAdapter(context, OnAlbumClickListener())
|
||||||
repository = AlbumsRepository(context, artistId)
|
repository = AlbumsRepository(context, artistId)
|
||||||
artistTracksRepository = ArtistTracksRepository(context, artistId)
|
artistTracksRepository = ArtistTracksRepository(context, artistId)
|
||||||
|
@ -132,19 +135,18 @@ class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||||
play.isClickable = false
|
play.isClickable = false
|
||||||
|
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
artistTracksRepository.fetch(Repository.Origin.Network.origin)
|
artistTracksRepository.fetch().untilNetwork(lifecycleScope) { _, _, _ ->
|
||||||
.map { it.data }
|
loader.stop()
|
||||||
.toList()
|
|
||||||
.flatten()
|
|
||||||
.shuffled()
|
|
||||||
.also {
|
|
||||||
CommandBus.send(Command.ReplaceQueue(it))
|
|
||||||
|
|
||||||
withContext(Main) {
|
play.icon = requireContext().getDrawable(R.drawable.play)
|
||||||
play.icon = requireContext().getDrawable(R.drawable.play)
|
play.isClickable = true
|
||||||
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.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.adapters.AlbumsGridAdapter
|
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.repositories.AlbumsRepository
|
||||||
import com.github.apognu.otter.utils.Album
|
|
||||||
import com.github.apognu.otter.utils.AppContext
|
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.*
|
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 viewRes = R.layout.fragment_albums_grid
|
||||||
override val recycler: RecyclerView get() = albums
|
override val recycler: RecyclerView get() = albums
|
||||||
override val layoutManager get() = GridLayoutManager(context, 3)
|
override val layoutManager get() = GridLayoutManager(context, 3)
|
||||||
|
|
|
@ -2,57 +2,66 @@ package com.github.apognu.otter.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import androidx.transition.Fade
|
import androidx.transition.Fade
|
||||||
import androidx.transition.Slide
|
import androidx.transition.Slide
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.adapters.ArtistsAdapter
|
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.repositories.ArtistsRepository
|
||||||
import com.github.apognu.otter.utils.AppContext
|
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.utils.onViewPager
|
||||||
|
import com.github.apognu.otter.models.domain.Artist
|
||||||
|
import com.github.apognu.otter.viewmodels.ArtistsViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
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 viewRes = R.layout.fragment_artists
|
||||||
override val recycler: RecyclerView get() = artists
|
override val recycler: RecyclerView get() = artists
|
||||||
override val alwaysRefresh = false
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {
|
fun openAlbums(context: Context?, artist: Artist?, fragment: Fragment? = null, art: String? = null) {
|
||||||
(context as? MainActivity)?.let {
|
artist?.let {
|
||||||
fragment?.let { fragment ->
|
(context as? MainActivity)?.let {
|
||||||
fragment.onViewPager {
|
fragment?.let { fragment ->
|
||||||
exitTransition = Fade().apply {
|
fragment.onViewPager {
|
||||||
duration = AppContext.TRANSITION_DURATION
|
exitTransition = Fade().apply {
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
duration = AppContext.TRANSITION_DURATION
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
|
||||||
view?.let {
|
view?.let {
|
||||||
addTarget(it)
|
addTarget(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
(context as? AppCompatActivity)?.let { activity ->
|
(context as? AppCompatActivity)?.let { activity ->
|
||||||
val nextFragment = AlbumsFragment.new(artist, art).apply {
|
val nextFragment = AlbumsFragment.new(artist, art).apply {
|
||||||
enterTransition = Slide().apply {
|
enterTransition = Slide().apply {
|
||||||
duration = AppContext.TRANSITION_DURATION
|
duration = AppContext.TRANSITION_DURATION
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
activity.supportFragmentManager
|
activity.supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.container, nextFragment)
|
.replace(R.id.container, nextFragment)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit()
|
.commit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,9 +73,28 @@ class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
|
||||||
repository = ArtistsRepository(context)
|
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 {
|
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
|
||||||
override fun onClick(holder: View?, artist: Artist) {
|
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 android.os.Bundle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.FavoritesAdapter
|
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.FavoritesRepository
|
||||||
import com.github.apognu.otter.repositories.TracksRepository
|
import com.github.apognu.otter.repositories.TracksRepository
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.Command
|
||||||
import com.google.android.exoplayer2.offline.Download
|
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.android.synthetic.main.fragment_favorites.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
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 viewRes = R.layout.fragment_favorites
|
||||||
override val recycler: RecyclerView get() = favorites
|
override val recycler: RecyclerView get() = favorites
|
||||||
override val alwaysRefresh = false
|
override val alwaysRefresh = false
|
||||||
|
@ -27,23 +32,14 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||||
adapter = FavoritesAdapter(context, FavoriteListener())
|
adapter = FavoritesAdapter(context, FavoriteListener())
|
||||||
repository = FavoritesRepository(context)
|
repository = FavoritesRepository(context)
|
||||||
|
|
||||||
|
PlayerStateViewModel.get().track.observe(this) { refreshCurrentTrack(it) }
|
||||||
|
|
||||||
watchEventBus()
|
watchEventBus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.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 {
|
play.setOnClickListener {
|
||||||
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||||
}
|
}
|
||||||
|
@ -53,15 +49,7 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
EventBus.get().collect { message ->
|
EventBus.get().collect { message ->
|
||||||
when (message) {
|
when (message) {
|
||||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
// is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
|
||||||
CommandBus.get().collect { command ->
|
|
||||||
when (command) {
|
|
||||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,17 +58,17 @@ class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||||
private suspend fun refreshDownloadedTracks() {
|
private suspend fun refreshDownloadedTracks() {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||||
|
|
||||||
withContext(Main) {
|
/* withContext(Main) {
|
||||||
adapter.data = adapter.data.map {
|
adapter.data = adapter.data.map {
|
||||||
it.downloaded = downloaded.contains(it.id)
|
it.downloaded = downloaded.contains(it.id)
|
||||||
it
|
it
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
/* private suspend fun refreshDownloadedTrack(download: Download) {
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
download.getMetadata()?.let { info ->
|
download.getMetadata()?.let { info ->
|
||||||
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
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?) {
|
private fun refreshCurrentTrack(track: Track?) {
|
||||||
track?.let {
|
track?.let {
|
||||||
adapter.currentTrack?.current = false
|
adapter.currentTrack = track
|
||||||
adapter.currentTrack = track.apply {
|
|
||||||
current = true
|
|
||||||
}
|
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,29 +5,35 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.TracksAdapter
|
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.*
|
||||||
import kotlinx.android.synthetic.main.partial_queue.view.*
|
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() {
|
class LandscapeQueueFragment : Fragment() {
|
||||||
private var adapter: TracksAdapter? = null
|
private var adapter: TracksAdapter? = null
|
||||||
|
|
||||||
|
private val viewModel = QueueViewModel.get()
|
||||||
|
lateinit var favoritesRepository: FavoritesRepository
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
watchEventBus()
|
favoritesRepository = FavoritesRepository(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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 {
|
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.layoutManager = LinearLayoutManager(context)
|
||||||
queue.adapter = it
|
queue.adapter = it
|
||||||
}
|
}
|
||||||
|
@ -39,43 +45,28 @@ class LandscapeQueueFragment : Fragment() {
|
||||||
|
|
||||||
queue?.visibility = View.GONE
|
queue?.visibility = View.GONE
|
||||||
placeholder?.visibility = View.VISIBLE
|
placeholder?.visibility = View.VISIBLE
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
private fun refresh(tracks: List<Track>) {
|
||||||
activity?.lifecycleScope?.launch(Main) {
|
adapter?.let {
|
||||||
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
|
it.data = tracks.toMutableList()
|
||||||
adapter?.let {
|
it.notifyDataSetChanged()
|
||||||
it.data = response.queue.toMutableList()
|
|
||||||
it.notifyDataSetChanged()
|
|
||||||
|
|
||||||
if (it.data.isEmpty()) {
|
if (it.data.isEmpty()) {
|
||||||
queue?.visibility = View.GONE
|
queue?.visibility = View.GONE
|
||||||
placeholder?.visibility = View.VISIBLE
|
placeholder?.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
queue?.visibility = View.VISIBLE
|
queue?.visibility = View.VISIBLE
|
||||||
placeholder?.visibility = View.GONE
|
placeholder?.visibility = View.GONE
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun watchEventBus() {
|
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
||||||
activity?.lifecycleScope?.launch(Main) {
|
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||||
EventBus.get().collect { message ->
|
when (state) {
|
||||||
when (message) {
|
true -> favoritesRepository.addFavorite(id)
|
||||||
is Event.QueueChanged -> refresh()
|
false -> favoritesRepository.deleteFavorite(id)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.lifecycleScope?.launch(Main) {
|
|
||||||
CommandBus.get().collect { command ->
|
|
||||||
when (command) {
|
|
||||||
is Command.RefreshTrack -> refresh()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,17 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.github.apognu.otter.repositories.HttpUpstream
|
import com.github.apognu.otter.repositories.HttpUpstream
|
||||||
import com.github.apognu.otter.repositories.Repository
|
import com.github.apognu.otter.repositories.Repository
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.Event
|
||||||
import com.google.gson.Gson
|
import com.github.apognu.otter.utils.EventBus
|
||||||
|
import com.github.apognu.otter.utils.untilNetwork
|
||||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
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 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 {
|
companion object {
|
||||||
const val OFFSCREEN_PAGES = 20
|
const val OFFSCREEN_PAGES = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract val liveData: LiveData<List<DAO>>
|
||||||
abstract val viewRes: Int
|
abstract val viewRes: Int
|
||||||
abstract val recycler: RecyclerView
|
abstract val recycler: RecyclerView
|
||||||
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
|
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
|
||||||
open val alwaysRefresh = true
|
open val alwaysRefresh = true
|
||||||
|
|
||||||
lateinit var repository: Repository<D, *>
|
lateinit var repository: Repository<D>
|
||||||
lateinit var adapter: A
|
lateinit var adapter: A
|
||||||
|
|
||||||
private var moreLoading = false
|
private var moreLoading = false
|
||||||
|
@ -58,7 +62,7 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||||
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
|
|
||||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||||
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||||
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||||
val offset = recycler.computeVerticalScrollOffset()
|
val offset = recycler.computeVerticalScrollOffset()
|
||||||
|
@ -66,7 +70,7 @@ abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||||
if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) {
|
if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) {
|
||||||
moreLoading = true
|
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) {
|
if (event is Event.ListingsChanged) {
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
swiper?.isRefreshing = true
|
swiper?.isRefreshing = true
|
||||||
fetch(Repository.Origin.Network.origin)
|
fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(Repository.Origin.Cache.origin)
|
fetch()
|
||||||
|
}
|
||||||
|
|
||||||
if (alwaysRefresh && adapter.data.isEmpty()) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
fetch(Repository.Origin.Network.origin)
|
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()
|
super.onResume()
|
||||||
|
|
||||||
swiper?.setOnRefreshListener {
|
swiper?.setOnRefreshListener {
|
||||||
fetch(Repository.Origin.Network.origin)
|
fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onDataFetched(data: List<D>) {}
|
open fun onDataFetched(data: List<D>) {}
|
||||||
|
|
||||||
private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
|
private fun fetch(size: Int = 0) {
|
||||||
var first = size == 0
|
|
||||||
|
|
||||||
if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
|
|
||||||
lifecycleScope.launch(Main) {
|
|
||||||
swiper?.isRefreshing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moreLoading = true
|
moreLoading = true
|
||||||
|
|
||||||
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
|
repository.fetch(size).untilNetwork(lifecycleScope, IO) { data, _, hasMore ->
|
||||||
if (isCache && data.isEmpty()) {
|
|
||||||
moreLoading = false
|
|
||||||
|
|
||||||
return@untilNetwork fetch(Repository.Origin.Network.origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
if (isCache) {
|
|
||||||
moreLoading = false
|
|
||||||
|
|
||||||
adapter.data = data.toMutableList()
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first) {
|
|
||||||
adapter.data.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDataFetched(data)
|
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) {
|
if (hasMore) {
|
||||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||||
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||||
if (first || needsMoreOffscreenPages()) {
|
if (size == 0 || needsMoreOffscreenPages()) {
|
||||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
fetch(size + data.size)
|
||||||
} else {
|
} else {
|
||||||
moreLoading = false
|
moreLoading = false
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
moreLoading = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||||
when (upstream.behavior) {
|
when (upstream.behavior) {
|
||||||
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
|
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
|
||||||
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
|
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
|
||||||
HttpUpstream.Behavior.Single -> 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 android.view.View
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.PlaylistTracksAdapter
|
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.FavoritesRepository
|
||||||
import com.github.apognu.otter.repositories.PlaylistTracksRepository
|
import com.github.apognu.otter.repositories.PlaylistTracksRepository
|
||||||
import com.github.apognu.otter.utils.*
|
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 com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.fragment_tracks.*
|
import kotlinx.android.synthetic.main.fragment_tracks.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
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 viewRes = R.layout.fragment_tracks
|
||||||
override val recycler: RecyclerView get() = tracks
|
override val recycler: RecyclerView get() = tracks
|
||||||
|
|
||||||
lateinit var favoritesRepository: FavoritesRepository
|
lateinit var favoritesRepository: FavoritesRepository
|
||||||
|
|
||||||
var albumId = 0
|
var playlistId = 0
|
||||||
var albumArtist = ""
|
var playlistName = ""
|
||||||
var albumTitle = ""
|
|
||||||
var albumCover = ""
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(playlist: Playlist): PlaylistTracksFragment {
|
fun new(playlist: PlaylistEntity): PlaylistTracksFragment {
|
||||||
return PlaylistTracksFragment().apply {
|
return PlaylistTracksFragment().apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
"albumId" to playlist.id,
|
"playlistId" to playlist.id,
|
||||||
"albumArtist" to "N/A",
|
"playlistName" to playlist.name
|
||||||
"albumTitle" to playlist.name,
|
|
||||||
"albumCover" to ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
albumId = getInt("albumId")
|
playlistId = getInt("playlistId")
|
||||||
albumArtist = getString("albumArtist") ?: ""
|
playlistName = getString("playlistName") ?: "N/A"
|
||||||
albumTitle = getString("albumTitle") ?: ""
|
|
||||||
albumCover = getString("albumCover") ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveData = PlaylistViewModel(playlistId).tracks
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
adapter = PlaylistTracksAdapter(context, FavoriteListener())
|
adapter = PlaylistTracksAdapter(context, FavoriteListener())
|
||||||
repository = PlaylistTracksRepository(context, albumId)
|
repository = PlaylistTracksRepository(context, playlistId)
|
||||||
favoritesRepository = FavoritesRepository(context)
|
favoritesRepository = FavoritesRepository(context)
|
||||||
|
|
||||||
watchEventBus()
|
PlayerStateViewModel.get().track.observe(this) { track ->
|
||||||
|
adapter.currentTrack = track
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -67,19 +73,12 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||||
covers.visibility = View.VISIBLE
|
covers.visibility = View.VISIBLE
|
||||||
|
|
||||||
artist.text = "Playlist"
|
artist.text = "Playlist"
|
||||||
title.text = albumTitle
|
title.text = playlistName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.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
|
var coverHeight: Float? = null
|
||||||
|
|
||||||
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
|
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
|
||||||
|
@ -95,7 +94,7 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||||
}
|
}
|
||||||
|
|
||||||
play.setOnClickListener {
|
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")
|
context.toast("All tracks were added to your queue")
|
||||||
}
|
}
|
||||||
|
@ -108,12 +107,12 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.add_to_queue -> {
|
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")
|
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
|
true
|
||||||
|
@ -125,8 +124,8 @@ class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDataFetched(data: List<PlaylistTrack>) {
|
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>) {
|
||||||
data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url ->
|
data.map { it.track.album }.toSet().map { it?.cover?.urls?.original }.take(4).forEachIndexed { index, url ->
|
||||||
val imageView = when (index) {
|
val imageView = when (index) {
|
||||||
0 -> cover_top_left
|
0 -> cover_top_left
|
||||||
1 -> cover_top_right
|
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 {
|
inner class FavoriteListener : PlaylistTracksAdapter.OnFavoriteListener {
|
||||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||||
when (state) {
|
when (state) {
|
||||||
|
|
|
@ -9,12 +9,15 @@ import androidx.transition.Slide
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.adapters.PlaylistsAdapter
|
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.repositories.PlaylistsRepository
|
||||||
import com.github.apognu.otter.utils.AppContext
|
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.*
|
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 viewRes = R.layout.fragment_playlists
|
||||||
override val recycler: RecyclerView get() = playlists
|
override val recycler: RecyclerView get() = playlists
|
||||||
override val alwaysRefresh = false
|
override val alwaysRefresh = false
|
||||||
|
@ -27,7 +30,7 @@ class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
|
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
|
||||||
override fun onClick(holder: View?, playlist: Playlist) {
|
override fun onClick(holder: View?, playlist: PlaylistEntity) {
|
||||||
(context as? MainActivity)?.let { activity ->
|
(context as? MainActivity)?.let { activity ->
|
||||||
exitTransition = Fade().apply {
|
exitTransition = Fade().apply {
|
||||||
duration = AppContext.TRANSITION_DURATION
|
duration = AppContext.TRANSITION_DURATION
|
||||||
|
|
|
@ -6,25 +6,26 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.TracksAdapter
|
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.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.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_queue.*
|
import kotlinx.android.synthetic.main.fragment_queue.*
|
||||||
import kotlinx.android.synthetic.main.fragment_queue.view.*
|
import kotlinx.android.synthetic.main.fragment_queue.view.*
|
||||||
import kotlinx.android.synthetic.main.partial_queue.*
|
import kotlinx.android.synthetic.main.partial_queue.*
|
||||||
import kotlinx.android.synthetic.main.partial_queue.view.*
|
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() {
|
class QueueFragment : BottomSheetDialogFragment() {
|
||||||
private var adapter: TracksAdapter? = null
|
private var adapter: TracksAdapter? = null
|
||||||
|
|
||||||
|
private val viewModel = QueueViewModel.get()
|
||||||
lateinit var favoritesRepository: FavoritesRepository
|
lateinit var favoritesRepository: FavoritesRepository
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -33,8 +34,6 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||||
favoritesRepository = FavoritesRepository(context)
|
favoritesRepository = FavoritesRepository(context)
|
||||||
|
|
||||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet)
|
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet)
|
||||||
|
|
||||||
watchEventBus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
@ -48,6 +47,10 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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 {
|
return inflater.inflate(R.layout.fragment_queue, container, false).apply {
|
||||||
adapter = TracksAdapter(context, FavoriteListener(), fromQueue = true).also {
|
adapter = TracksAdapter(context, FavoriteListener(), fromQueue = true).also {
|
||||||
included.queue.layoutManager = LinearLayoutManager(context)
|
included.queue.layoutManager = LinearLayoutManager(context)
|
||||||
|
@ -69,44 +72,20 @@ class QueueFragment : BottomSheetDialogFragment() {
|
||||||
queue_clear.setOnClickListener {
|
queue_clear.setOnClickListener {
|
||||||
CommandBus.send(Command.ClearQueue)
|
CommandBus.send(Command.ClearQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
private fun refresh(tracks: List<Track>) {
|
||||||
lifecycleScope.launch(Main) {
|
included?.let { included ->
|
||||||
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
|
adapter?.let {
|
||||||
included?.let { included ->
|
it.data = tracks.toMutableList()
|
||||||
adapter?.let {
|
it.notifyDataSetChanged()
|
||||||
it.data = response.queue.toMutableList()
|
|
||||||
it.notifyDataSetChanged()
|
|
||||||
|
|
||||||
if (it.data.isEmpty()) {
|
if (it.data.isEmpty()) {
|
||||||
included.queue?.visibility = View.GONE
|
included.queue?.visibility = View.GONE
|
||||||
placeholder?.visibility = View.VISIBLE
|
placeholder?.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
included.queue?.visibility = View.VISIBLE
|
included.queue?.visibility = View.VISIBLE
|
||||||
placeholder?.visibility = View.GONE
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,18 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.RadiosAdapter
|
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.repositories.RadiosRepository
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
|
import com.github.apognu.otter.viewmodels.RadiosViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_radios.*
|
import kotlinx.android.synthetic.main.fragment_radios.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
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 viewRes = R.layout.fragment_radios
|
||||||
override val recycler: RecyclerView get() = radios
|
override val recycler: RecyclerView get() = radios
|
||||||
override val alwaysRefresh = false
|
override val alwaysRefresh = false
|
||||||
|
@ -26,13 +30,14 @@ class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
|
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
|
||||||
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: Radio) {
|
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: RadioEntity) {
|
||||||
holder.spin()
|
holder.spin()
|
||||||
recycler.forEach {
|
recycler.forEach {
|
||||||
it.isEnabled = false
|
it.isEnabled = false
|
||||||
it.isClickable = false
|
it.isClickable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TOBEREDONE
|
||||||
CommandBus.send(Command.PlayRadio(radio))
|
CommandBus.send(Command.PlayRadio(radio))
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
|
|
|
@ -11,9 +11,9 @@ import android.widget.TextView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.github.apognu.otter.R
|
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.mustNormalizeUrl
|
||||||
import com.github.apognu.otter.utils.toDurationString
|
import com.github.apognu.otter.utils.toDurationString
|
||||||
|
import com.github.apognu.otter.models.domain.Track
|
||||||
import kotlinx.android.synthetic.main.fragment_track_info_details.*
|
import kotlinx.android.synthetic.main.fragment_track_info_details.*
|
||||||
|
|
||||||
class TrackInfoDetailsFragment : DialogFragment() {
|
class TrackInfoDetailsFragment : DialogFragment() {
|
||||||
|
@ -21,7 +21,7 @@ class TrackInfoDetailsFragment : DialogFragment() {
|
||||||
fun new(track: Track): TrackInfoDetailsFragment {
|
fun new(track: Track): TrackInfoDetailsFragment {
|
||||||
return TrackInfoDetailsFragment().apply {
|
return TrackInfoDetailsFragment().apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
"artistName" to track.artist.name,
|
"artistName" to track.artist?.name,
|
||||||
"albumTitle" to track.album?.title,
|
"albumTitle" to track.album?.title,
|
||||||
"trackTitle" to track.title,
|
"trackTitle" to track.title,
|
||||||
"trackCopyright" to track.copyright,
|
"trackCopyright" to track.copyright,
|
||||||
|
|
|
@ -6,14 +6,20 @@ import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.TracksAdapter
|
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.FavoritedRepository
|
||||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||||
import com.github.apognu.otter.repositories.TracksRepository
|
import com.github.apognu.otter.repositories.TracksRepository
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
|
import com.github.apognu.otter.viewmodels.*
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
|
@ -25,7 +31,8 @@ import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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 viewRes = R.layout.fragment_tracks
|
||||||
override val recycler: RecyclerView get() = tracks
|
override val recycler: RecyclerView get() = tracks
|
||||||
|
|
||||||
|
@ -40,26 +47,36 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||||
companion object {
|
companion object {
|
||||||
fun new(album: Album): TracksFragment {
|
fun new(album: Album): TracksFragment {
|
||||||
return TracksFragment().apply {
|
return TracksFragment().apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf("albumId" to album.id)
|
||||||
"albumId" to album.id,
|
|
||||||
"albumArtist" to album.artist.name,
|
|
||||||
"albumTitle" to album.title,
|
|
||||||
"albumCover" to album.cover()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
albumId = getInt("albumId")
|
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())
|
adapter = TracksAdapter(context, FavoriteListener())
|
||||||
repository = TracksRepository(context, albumId)
|
repository = TracksRepository(context, albumId)
|
||||||
favoritesRepository = FavoritesRepository(context)
|
favoritesRepository = FavoritesRepository(context)
|
||||||
|
@ -68,33 +85,9 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||||
watchEventBus()
|
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() {
|
override fun onResume() {
|
||||||
super.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
|
var coverHeight: Float? = null
|
||||||
|
|
||||||
scroller.setOnScrollChangeListener { _: View?, _: Int, scrollY: Int, _: Int, _: Int ->
|
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() {
|
private suspend fun refreshDownloadedTracks() {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||||
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
adapter.data = adapter.data.map {
|
/* adapter.data = adapter.data.map {
|
||||||
it.downloaded = downloaded.contains(it.id)
|
it.downloaded = downloaded.contains(it.id)
|
||||||
it
|
it
|
||||||
}.toMutableList()
|
}.toMutableList() */
|
||||||
|
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
@ -198,26 +183,15 @@ class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
download.getMetadata()?.let { info ->
|
download.getMetadata()?.let { info ->
|
||||||
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
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.data[match.second].downloaded = true
|
||||||
adapter.notifyItemChanged(match.second)
|
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 {
|
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
||||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||||
when (state) {
|
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?
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.utils.AppContext
|
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.utils.maybeNormalizeUrl
|
||||||
|
import com.github.apognu.otter.models.domain.Track
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.Default
|
import kotlinx.coroutines.Dispatchers.Default
|
||||||
|
@ -68,7 +68,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
.setContentTitle(track.title)
|
.setContentTitle(track.title)
|
||||||
.setContentText(track.artist.name)
|
.setContentText(track.artist?.name)
|
||||||
.setContentIntent(openPendingIntent)
|
.setContentIntent(openPendingIntent)
|
||||||
.setChannelId(AppContext.NOTIFICATION_CHANNEL_MEDIA_CONTROL)
|
.setChannelId(AppContext.NOTIFICATION_CHANNEL_MEDIA_CONTROL)
|
||||||
.addAction(
|
.addAction(
|
||||||
|
|
|
@ -6,7 +6,10 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
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.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.Download
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.offline.DownloadRequest
|
import com.google.android.exoplayer2.offline.DownloadRequest
|
||||||
|
@ -17,8 +20,6 @@ import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
|
@ -33,7 +34,7 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
track.id,
|
track.id,
|
||||||
url,
|
url,
|
||||||
track.title,
|
track.title,
|
||||||
track.artist.name,
|
track.artist?.name ?: "",
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
).toByteArray()
|
).toByteArray()
|
||||||
|
@ -48,17 +49,15 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
buildResumeDownloadsIntent(this, PinService::class.java, true)
|
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)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
DownloadsViewModel.get().cursor.postValue(getDownloads())
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDownloadManager() = Otter.get().exoDownloadManager.apply {
|
override fun getDownloadManager() = Otter.get().exoDownloadManager.apply {
|
||||||
addListener(DownloadListener())
|
addListener(DownloadListener())
|
||||||
}
|
}
|
||||||
|
@ -77,7 +76,15 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download) {
|
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download) {
|
||||||
super.onDownloadChanged(downloadManager, 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 androidx.media.session.MediaButtonReceiver
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
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.utils.*
|
||||||
|
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException
|
import com.google.android.exoplayer2.ExoPlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
|
@ -133,7 +136,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
val (current, duration, percent) = getProgress(true)
|
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 ->
|
CommandBus.get().collect { command ->
|
||||||
when (command) {
|
when (command) {
|
||||||
is Command.RefreshService -> {
|
is Command.RefreshService -> {
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
|
|
||||||
if (queue.metadata.isNotEmpty()) {
|
if (queue.metadata.isNotEmpty()) {
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
PlayerStateViewModel.get()._track.postValue(queue.current())
|
||||||
EventBus.send(Event.StateChanged(player.playWhenReady))
|
PlayerStateViewModel.get().isPlaying.postValue(player.playWhenReady)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +194,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
is Command.PlayRadio -> {
|
is Command.PlayRadio -> {
|
||||||
queue.clear()
|
queue.clear()
|
||||||
|
// TOBEREDONE
|
||||||
radioPlayer.play(command.radio)
|
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) {
|
scope.launch(Main) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
|
|
||||||
val (current, duration, percent) = getProgress()
|
|
||||||
|
|
||||||
if (player.playWhenReady) {
|
if (player.playWhenReady) {
|
||||||
ProgressBus.send(current, duration, percent)
|
PlayerStateViewModel.get().position.postValue(getProgress())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +271,7 @@ class PlayerService : Service() {
|
||||||
if (hasAudioFocus(state)) {
|
if (hasAudioFocus(state)) {
|
||||||
player.playWhenReady = state
|
player.playWhenReady = state
|
||||||
|
|
||||||
EventBus.send(Event.StateChanged(state))
|
PlayerStateViewModel.get().isPlaying.postValue(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +291,7 @@ class PlayerService : Service() {
|
||||||
player.next()
|
player.next()
|
||||||
|
|
||||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
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> {
|
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||||
|
@ -331,7 +321,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
return mediaMetadataBuilder.apply {
|
return mediaMetadataBuilder.apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
|
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)
|
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -381,24 +371,22 @@ class PlayerService : Service() {
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
super.onPlayerStateChanged(playWhenReady, playbackState)
|
||||||
|
|
||||||
EventBus.send(Event.StateChanged(playWhenReady))
|
PlayerStateViewModel.get().isPlaying.postValue(playWhenReady)
|
||||||
|
|
||||||
if (queue.current == -1) {
|
if (queue.current == -1) {
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
PlayerStateViewModel.get()._track.postValue(queue.current())
|
||||||
}
|
}
|
||||||
|
|
||||||
when (playWhenReady) {
|
when (playWhenReady) {
|
||||||
true -> {
|
true -> {
|
||||||
when (playbackState) {
|
when (playbackState) {
|
||||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
|
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 -> {
|
Player.STATE_ENDED -> {
|
||||||
setPlaybackState(false)
|
setPlaybackState(false)
|
||||||
|
|
||||||
queue.current = 0
|
queue.current = 0
|
||||||
player.seekTo(0, C.TIME_UNSET)
|
player.seekTo(0, C.TIME_UNSET)
|
||||||
|
|
||||||
ProgressBus.send(0, 0, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Player.STATE_IDLE -> {
|
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 -> {
|
false -> {
|
||||||
EventBus.send(Event.Buffering(false))
|
PlayerStateViewModel.get().isBuffering.postValue(false)
|
||||||
|
|
||||||
Build.VERSION_CODES.N.onApi(
|
Build.VERSION_CODES.N.onApi(
|
||||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||||
|
@ -446,7 +434,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray())
|
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) {
|
override fun onPositionDiscontinuity(reason: Int) {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
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.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.ConcatenatingMediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
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.CacheDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
||||||
import com.google.android.exoplayer2.util.Util
|
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) {
|
class QueueManager(val context: Context) {
|
||||||
|
private val queueRepository = QueueRepository(GlobalScope)
|
||||||
|
|
||||||
var metadata: MutableList<Track> = mutableListOf()
|
var metadata: MutableList<Track> = mutableListOf()
|
||||||
val datasources = ConcatenatingMediaSource()
|
val datasources = ConcatenatingMediaSource()
|
||||||
var current = -1
|
var current = -1
|
||||||
|
@ -44,34 +50,24 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Cache.get(context, "queue")?.let { json ->
|
GlobalScope.launch(IO) {
|
||||||
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
queueRepository.allBlocking().also {
|
||||||
metadata = cache.data.toMutableList()
|
replace(it.map { Track.fromDecoratedEntity(it) })
|
||||||
|
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.get(context, "current")?.let { string ->
|
Cache.get(context, "current")?.let { string ->
|
||||||
current = string.readLine().toInt()
|
current = string.readLine().toInt()
|
||||||
|
|
||||||
|
PlayerStateViewModel.get()._track.postValue(current())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persist() {
|
private fun persist() = queueRepository.replace(metadata)
|
||||||
Cache.set(
|
|
||||||
context,
|
|
||||||
"queue",
|
|
||||||
Gson().toJson(QueueCache(metadata)).toByteArray()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replace(tracks: List<Track>) {
|
fun replace(tracks: List<Track>) {
|
||||||
|
metadata = tracks.toMutableList()
|
||||||
|
|
||||||
val factory = factory(context)
|
val factory = factory(context)
|
||||||
|
|
||||||
val sources = tracks.map { track ->
|
val sources = tracks.map { track ->
|
||||||
|
@ -80,13 +76,10 @@ class QueueManager(val context: Context) {
|
||||||
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata = tracks.toMutableList()
|
|
||||||
datasources.clear()
|
datasources.clear()
|
||||||
datasources.addMediaSources(sources)
|
datasources.addMediaSources(sources)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun append(tracks: List<Track>) {
|
fun append(tracks: List<Track>) {
|
||||||
|
@ -103,8 +96,6 @@ class QueueManager(val context: Context) {
|
||||||
datasources.addMediaSources(sources)
|
datasources.addMediaSources(sources)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertNext(track: Track) {
|
fun insertNext(track: Track) {
|
||||||
|
@ -121,8 +112,6 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(track: Track) {
|
fun remove(track: Track) {
|
||||||
|
@ -148,8 +137,6 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun move(oldPosition: Int, newPosition: Int) {
|
fun move(oldPosition: Int, newPosition: Int) {
|
||||||
|
@ -159,10 +146,7 @@ class QueueManager(val context: Context) {
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get() = metadata.mapIndexed { index, track ->
|
fun get() = metadata
|
||||||
track.current = index == current
|
|
||||||
track
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(index: Int): Track = metadata[index]
|
fun get(index: Int): Track = metadata[index]
|
||||||
|
|
||||||
|
@ -205,7 +189,5 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
EventBus.send(Event.QueueChanged)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,34 +1,44 @@
|
||||||
package com.github.apognu.otter.playback
|
package com.github.apognu.otter.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.R
|
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.FavoritedRepository
|
||||||
import com.github.apognu.otter.repositories.Repository
|
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.withContext
|
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)
|
data class RadioSession(val id: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class RadioTrackBody(val session: Int)
|
data class RadioTrackBody(val session: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class RadioTrackID(val id: Int)
|
data class RadioTrackID(val id: Int)
|
||||||
|
|
||||||
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
val lock = Semaphore(1)
|
val lock = Semaphore(1)
|
||||||
|
|
||||||
private var currentRadio: Radio? = null
|
private var currentRadio: RadioEntity? = null
|
||||||
private var session: Int? = null
|
private var session: Int? = null
|
||||||
private var cookie: String? = 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 ->
|
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
||||||
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
|
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
|
||||||
|
|
||||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
currentRadio = RadioEntity(radio_id, radio_type, "", "")
|
||||||
session = radio_session
|
session = radio_session
|
||||||
cookie = cachedCookie
|
cookie = cachedCookie
|
||||||
}
|
}
|
||||||
|
@ -48,7 +58,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(radio: Radio) {
|
fun play(radio: RadioEntity) {
|
||||||
currentRadio = radio
|
currentRadio = radio
|
||||||
session = null
|
session = null
|
||||||
|
|
||||||
|
@ -70,6 +80,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
fun isActive() = currentRadio != null && session != null
|
fun isActive() = currentRadio != null && session != null
|
||||||
|
|
||||||
private suspend fun createSession() {
|
private suspend fun createSession() {
|
||||||
|
"createSession".log()
|
||||||
|
|
||||||
currentRadio?.let { radio ->
|
currentRadio?.let { radio ->
|
||||||
try {
|
try {
|
||||||
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
|
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()
|
.authorize()
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(body)
|
.body(body)
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
|
.awaitObjectResponseResult<RadioSession>(AppContext.deserializer())
|
||||||
|
|
||||||
session = result.get().id
|
session = result.get().id
|
||||||
cookie = response.header("set-cookie").joinToString(";")
|
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_id", radio.id.toString().toByteArray())
|
||||||
Cache.set(context, "radio_session", session.toString().toByteArray())
|
Cache.set(context, "radio_session", session.toString().toByteArray())
|
||||||
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
||||||
|
|
||||||
prepareNextTrack(true)
|
prepareNextTrack(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.log()
|
||||||
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
context.toast(context.getString(R.string.radio_playback_error))
|
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) {
|
suspend fun prepareNextTrack(first: Boolean = false) {
|
||||||
|
"prepareTrack".log()
|
||||||
|
|
||||||
session?.let { session ->
|
session?.let { session ->
|
||||||
try {
|
try {
|
||||||
val body = Gson().toJson(RadioTrackBody(session))
|
val body = Gson().toJson(RadioTrackBody(session))
|
||||||
|
@ -115,25 +131,23 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.body(body)
|
.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()
|
.authorize()
|
||||||
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
.awaitObjectResult<FunkwhaleTrack>(AppContext.deserializer())
|
||||||
|
.get()
|
||||||
|
|
||||||
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
|
Otter.get().database.tracks().run {
|
||||||
.map { it.data }
|
insertWithAssocs(track)
|
||||||
.toList()
|
|
||||||
.flatten()
|
|
||||||
|
|
||||||
val track = trackResponse.get().apply {
|
Track.fromDecoratedEntity(find(track.id)).let { track ->
|
||||||
favorite = favorites.contains(id)
|
if (first) {
|
||||||
}
|
CommandBus.send(Command.ReplaceQueue(listOf(track), true))
|
||||||
|
} else {
|
||||||
if (first) {
|
CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||||
CommandBus.send(Command.ReplaceQueue(listOf(track), true))
|
}
|
||||||
} else {
|
}
|
||||||
CommandBus.send(Command.AddToQueue(listOf(track)))
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
|
|
|
@ -1,32 +1,28 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.Album
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.AlbumsCache
|
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||||
import com.github.apognu.otter.utils.AlbumsResponse
|
import com.github.apognu.otter.models.dao.toDao
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository<Album, AlbumsCache>() {
|
class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository<FunkwhaleAlbum>() {
|
||||||
override val cacheId: String by lazy {
|
override val upstream: Upstream<FunkwhaleAlbum> by lazy {
|
||||||
if (artistId == null) "albums"
|
|
||||||
else "albums-artist-$artistId"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val upstream: Upstream<Album> by lazy {
|
|
||||||
val url =
|
val url =
|
||||||
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
|
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
|
||||||
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
|
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
|
||||||
|
|
||||||
HttpUpstream<Album, OtterResponse<Album>>(
|
HttpUpstream(
|
||||||
HttpUpstream.Behavior.Progressive,
|
HttpUpstream.Behavior.Progressive,
|
||||||
url,
|
url,
|
||||||
object : TypeToken<AlbumsResponse>() {}.type
|
FunkwhaleAlbum.serializer()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
|
data.forEach {
|
||||||
|
Otter.get().database.albums().insert(it.toDao())
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onDataFetched(data)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.Track
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
import com.github.apognu.otter.utils.TracksCache
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import com.github.apognu.otter.utils.TracksResponse
|
import kotlinx.coroutines.runBlocking
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
|
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<FunkwhaleTrack>() {
|
||||||
override val cacheId = "tracks-artist-$artistId"
|
override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", FunkwhaleTrack.serializer())
|
||||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
|
||||||
|
|
||||||
override fun cache(data: List<Track>) = TracksCache(data)
|
override fun onDataFetched(data: List<FunkwhaleTrack>) = runBlocking(IO) {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
data.forEach {
|
||||||
|
Otter.get().database.tracks().insertWithAssocs(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDataFetched(data)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,33 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.Artist
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.ArtistsCache
|
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||||
import com.github.apognu.otter.utils.ArtistsResponse
|
import com.github.apognu.otter.models.dao.toDao
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.models.dao.toRealmDao
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import io.realm.Realm
|
||||||
import com.google.gson.reflect.TypeToken
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import java.io.BufferedReader
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
class ArtistsRepository(override val context: Context?) : Repository<FunkwhaleArtist>() {
|
||||||
override val cacheId = "artists"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
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
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.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.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
class FavoritesRepository(override val context: Context?) : Repository<FunkwhaleTrack>() {
|
||||||
override val cacheId = "favorites.v2"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", FunkwhaleTrack.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Track>) = TracksCache(data)
|
val favoritedRepository = FavoritedRepository(context)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
|
||||||
|
|
||||||
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 ->
|
data.map { track ->
|
||||||
track.favorite = true
|
track.favorite = true
|
||||||
|
@ -39,10 +40,14 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
}
|
}
|
||||||
|
|
||||||
track
|
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 body = mapOf("track" to id)
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
|
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))
|
.body(Gson().toJson(body))
|
||||||
.awaitByteArrayResponseResult()
|
.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 body = mapOf("track" to id)
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
|
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))
|
.body(Gson().toJson(body))
|
||||||
.awaitByteArrayResponseResult()
|
.awaitByteArrayResponseResult()
|
||||||
|
|
||||||
favoritedRepository.update(context, scope)
|
favoritedRepository.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
class FavoritedRepository(override val context: Context?) : Repository<Favorited>() {
|
||||||
override val cacheId = "favorited"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
override fun onDataFetched(data: List<Favorited>): List<Favorited> {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
scope.launch(IO) {
|
||||||
|
data.forEach {
|
||||||
fun update(context: Context?, scope: CoroutineScope) {
|
Otter.get().database.favorites().insert(FavoriteEntity(it.track))
|
||||||
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
|
}
|
||||||
Cache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return super.onDataFetched(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update() = scope.launch(IO) {
|
||||||
|
fetch().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.net.Uri
|
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.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.core.FuelError
|
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.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import com.google.gson.Gson
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import java.io.Reader
|
import kotlinx.serialization.KSerializer
|
||||||
import java.lang.reflect.Type
|
|
||||||
import kotlin.math.ceil
|
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 {
|
enum class Behavior {
|
||||||
Single, AtOnce, Progressive
|
Single, AtOnce, Progressive
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
|
override fun fetch(size: Int): Flow<Repository.Response<D>> = channelFlow {
|
||||||
if (behavior == Behavior.Single && size != 0) return@flow
|
if (behavior == Behavior.Single && size != 0) return@channelFlow
|
||||||
|
|
||||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
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(
|
get(url).fold(
|
||||||
{ response ->
|
{ response ->
|
||||||
val data = response.getData()
|
val data = response.results
|
||||||
|
|
||||||
when (behavior) {
|
when (behavior) {
|
||||||
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
|
Behavior.Single -> send(Repository.Response(data, page, false))
|
||||||
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
Behavior.Progressive -> send(Repository.Response(data, page, response.next != null))
|
||||||
|
|
||||||
else -> {
|
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 ->
|
{ error ->
|
||||||
|
"GET $url".log()
|
||||||
|
error.log()
|
||||||
|
|
||||||
when (error.exception) {
|
when (error.exception) {
|
||||||
is RefreshError -> EventBus.send(Event.LogOut)
|
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)
|
}.flowOn(IO)
|
||||||
|
|
||||||
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
|
suspend fun get(url: String): Result<OtterResponse<D>, FuelError> {
|
||||||
override fun deserialize(reader: Reader): T? {
|
|
||||||
return Gson().fromJson(reader, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun get(url: String): Result<R, FuelError> {
|
|
||||||
return try {
|
return try {
|
||||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
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) {
|
if (response.statusCode == 401) {
|
||||||
return retryGet(url)
|
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 try {
|
||||||
return if (HTTP.refresh()) {
|
return if (HTTP.refresh()) {
|
||||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
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 {
|
} else {
|
||||||
Result.Failure(FuelError.wrap(RefreshError))
|
Result.Failure(FuelError.wrap(RefreshError))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,24 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.PlaylistTrack
|
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack
|
||||||
import com.github.apognu.otter.utils.PlaylistTracksCache
|
import com.github.apognu.otter.models.dao.PlaylistTrack
|
||||||
import com.github.apognu.otter.utils.PlaylistTracksResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
class PlaylistTracksRepository(override val context: Context?, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() {
|
||||||
override val cacheId = "tracks-playlist-$playlistId"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>): List<FunkwhalePlaylistTrack> = runBlocking {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
Otter.get().database.playlists().replaceTracks(playlistId, data.map {
|
||||||
|
Otter.get().database.tracks().insertWithAssocs(it.track)
|
||||||
|
|
||||||
override fun onDataFetched(data: List<PlaylistTrack>): List<PlaylistTrack> = runBlocking {
|
PlaylistTrack(playlistId, it.track.id)
|
||||||
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
|
})
|
||||||
.map { it.data }
|
|
||||||
.toList()
|
|
||||||
.flatten()
|
|
||||||
|
|
||||||
data.map { track ->
|
data
|
||||||
track.track.favorite = favorites.contains(track.track.id)
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.Playlist
|
import com.github.apognu.otter.models.api.FunkwhalePlaylist
|
||||||
import com.github.apognu.otter.utils.PlaylistsCache
|
import com.github.apognu.otter.models.dao.toDao
|
||||||
import com.github.apognu.otter.utils.PlaylistsResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
class PlaylistsRepository(override val context: Context?) : Repository<FunkwhalePlaylist>() {
|
||||||
override val cacheId = "tracks-playlists"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
override fun onDataFetched(data: List<FunkwhalePlaylist>): List<FunkwhalePlaylist> {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
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
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.OtterResponse
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.Radio
|
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
||||||
import com.github.apognu.otter.utils.RadiosCache
|
import com.github.apognu.otter.models.dao.toDao
|
||||||
import com.github.apognu.otter.utils.RadiosResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
class RadiosRepository(override val context: Context?) : Repository<FunkwhaleRadio>() {
|
||||||
override val cacheId = "radios"
|
override val upstream =
|
||||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
|
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
override fun onDataFetched(data: List<FunkwhaleRadio>): List<FunkwhaleRadio> {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)
|
data.forEach {
|
||||||
|
Otter.get().database.radios().insert(it.toDao())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDataFetched(data: List<Radio>): List<Radio> {
|
|
||||||
return data
|
return data
|
||||||
.map { radio -> radio.apply { radio_type = "custom" } }
|
.map { radio -> radio.apply { radio_type = "custom" } }
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
|
|
|
@ -1,59 +1,31 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.utils.AppContext
|
|
||||||
import com.github.apognu.otter.utils.Cache
|
|
||||||
import com.github.apognu.otter.utils.CacheItem
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.io.BufferedReader
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlin.math.ceil
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
interface Upstream<D> {
|
interface Upstream<D> {
|
||||||
fun fetch(size: Int = 0): Flow<Repository.Response<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)
|
protected val scope: CoroutineScope = CoroutineScope(Job() + IO)
|
||||||
|
|
||||||
enum class Origin(val origin: Int) {
|
data class Response<D>(val data: List<D>, val page: Int, val hasMore: Boolean)
|
||||||
Cache(0b01),
|
|
||||||
Network(0b10)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)
|
|
||||||
|
|
||||||
abstract val context: Context?
|
abstract val context: Context?
|
||||||
abstract val cacheId: String?
|
|
||||||
abstract val upstream: Upstream<D>
|
abstract val upstream: Upstream<D>
|
||||||
|
|
||||||
open fun cache(data: List<D>): C? = null
|
fun fetch(size: Int = 0) = channelFlow {
|
||||||
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 {
|
|
||||||
upstream
|
upstream
|
||||||
.fetch(size)
|
.fetch(size)
|
||||||
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
|
.map { response -> Response(onDataFetched(response.data), response.page, response.hasMore) }
|
||||||
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
|
.collect { response -> send(Response(response.data, response.page, response.hasMore)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onDataFetched(data: List<D>) = data
|
protected open fun onDataFetched(data: List<D>) = data
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.models.api.FunkwhaleAlbum
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
|
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleTrack>() {
|
||||||
override val cacheId: String? = null
|
override val upstream: Upstream<FunkwhaleTrack>
|
||||||
override val upstream: Upstream<Track>
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
|
||||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
|
||||||
|
|
||||||
override fun cache(data: List<Track>) = TracksCache(data)
|
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
val favorites = FavoritedRepository(context).fetch()
|
||||||
|
|
||||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
|
||||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
|
||||||
.map { it.data }
|
.map { it.data }
|
||||||
.toList()
|
.toList()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||||
|
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = favorites.contains(track.id)
|
track.favorite = favorites.contains(track.id)
|
||||||
|
@ -37,24 +31,18 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
track
|
track
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
|
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleArtist>() {
|
||||||
override val cacheId: String? = null
|
override val upstream: Upstream<FunkwhaleArtist>
|
||||||
override val upstream: Upstream<Artist>
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer())
|
||||||
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 AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
|
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleAlbum>() {
|
||||||
override val cacheId: String? = null
|
override val upstream: Upstream<FunkwhaleAlbum>
|
||||||
override val upstream: Upstream<Album>
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", FunkwhaleAlbum.serializer())
|
||||||
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)
|
|
||||||
}
|
}
|
|
@ -2,21 +2,14 @@ package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.apognu.otter.Otter
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.apognu.otter.utils.getMetadata
|
||||||
import com.google.android.exoplayer2.offline.Download
|
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 kotlinx.coroutines.runBlocking
|
||||||
import java.io.BufferedReader
|
|
||||||
|
|
||||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
class TracksRepository(override val context: Context?, albumId: Int) : Repository<FunkwhaleTrack>() {
|
||||||
override val cacheId = "tracks-album-$albumId"
|
override val upstream =
|
||||||
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)
|
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer())
|
||||||
|
|
||||||
override fun cache(data: List<Track>) = TracksCache(data)
|
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getDownloadedIds(): List<Int>? {
|
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 {
|
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
||||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
data.forEach { track ->
|
||||||
.map { it.data }
|
Otter.get().database.tracks().insertWithAssocs(track)
|
||||||
.toList()
|
}
|
||||||
.flatten()
|
|
||||||
|
|
||||||
val downloaded = getDownloadedIds() ?: listOf()
|
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
||||||
|
|
||||||
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 }))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,6 +11,12 @@ import android.os.Build
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.kittinunf.fuel.core.FuelManager
|
import com.github.kittinunf.fuel.core.FuelManager
|
||||||
import com.github.kittinunf.fuel.core.Method
|
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 {
|
object AppContext {
|
||||||
const val PREFS_CREDENTIALS = "credentials"
|
const val PREFS_CREDENTIALS = "credentials"
|
||||||
|
@ -23,6 +29,12 @@ object AppContext {
|
||||||
const val PAGE_SIZE = 50
|
const val PAGE_SIZE = 50
|
||||||
const val TRANSITION_DURATION = 300L
|
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) {
|
fun init(context: Activity) {
|
||||||
setupNotificationChannels(context)
|
setupNotificationChannels(context)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package com.github.apognu.otter.utils
|
package com.github.apognu.otter.utils
|
||||||
|
|
||||||
import com.github.apognu.otter.Otter
|
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.Download
|
||||||
import com.google.android.exoplayer2.offline.DownloadCursor
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Command {
|
sealed class Command {
|
||||||
|
@ -28,7 +27,7 @@ sealed class Command {
|
||||||
class MoveFromQueue(val oldPosition: Int, val newPosition: Int) : Command()
|
class MoveFromQueue(val oldPosition: Int, val newPosition: Int) : Command()
|
||||||
object ClearQueue : Command()
|
object ClearQueue : Command()
|
||||||
object ShuffleQueue : Command()
|
object ShuffleQueue : Command()
|
||||||
class PlayRadio(val radio: Radio) : Command()
|
class PlayRadio(val radio: RadioEntity) : Command()
|
||||||
|
|
||||||
class SetRepeatMode(val mode: Int) : Command()
|
class SetRepeatMode(val mode: Int) : Command()
|
||||||
|
|
||||||
|
@ -44,29 +43,12 @@ sealed class Event {
|
||||||
|
|
||||||
class PlaybackError(val message: String) : Event()
|
class PlaybackError(val message: String) : Event()
|
||||||
object PlaybackStopped : Event()
|
object PlaybackStopped : Event()
|
||||||
class Buffering(val value: Boolean) : Event()
|
|
||||||
class TrackFinished(val track: Track?) : Event()
|
class TrackFinished(val track: Track?) : Event()
|
||||||
class StateChanged(val playing: Boolean) : Event()
|
|
||||||
object QueueChanged : Event()
|
|
||||||
object RadioStarted : Event()
|
object RadioStarted : Event()
|
||||||
object ListingsChanged : Event()
|
object ListingsChanged : Event()
|
||||||
class DownloadChanged(val download: Download) : 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 {
|
object EventBus {
|
||||||
fun send(event: Event) {
|
fun send(event: Event) {
|
||||||
GlobalScope.launch(IO) {
|
GlobalScope.launch(IO) {
|
||||||
|
@ -87,33 +69,3 @@ object CommandBus {
|
||||||
fun get() = Otter.get().commandBus.asFlow()
|
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
|
package com.github.apognu.otter.utils
|
||||||
|
|
||||||
import android.content.Context
|
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.Fuel
|
||||||
import com.github.kittinunf.fuel.core.FuelError
|
import com.github.kittinunf.fuel.core.FuelError
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
@ -23,7 +22,9 @@ object HTTP {
|
||||||
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("password")
|
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("password")
|
||||||
).toList()
|
).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(
|
return result.fold(
|
||||||
{ data ->
|
{ 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) {
|
if (response.statusCode == 401) {
|
||||||
return retryGet(url)
|
return retryGet(url)
|
||||||
|
@ -59,7 +60,7 @@ object HTTP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
request.awaitObjectResult(AppContext.deserializer())
|
||||||
} else {
|
} else {
|
||||||
Result.Failure(FuelError.wrap(RefreshError))
|
Result.Failure(FuelError.wrap(RefreshError))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Build
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.BrowseFragment
|
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.apognu.otter.repositories.Repository
|
||||||
import com.github.kittinunf.fuel.core.Request
|
import com.github.kittinunf.fuel.core.Request
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
@ -17,10 +18,10 @@ import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.CoroutineContext
|
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) {
|
scope.launch(context) {
|
||||||
collect { data ->
|
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
|
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.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ object Userinfo {
|
||||||
val hostname = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
val hostname = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
||||||
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
||||||
.authorize()
|
.authorize()
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
|
.awaitObjectResponseResult<User>(AppContext.deserializer())
|
||||||
|
|
||||||
return when (result) {
|
return when (result) {
|
||||||
is Result.Success -> {
|
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:baselineAligned="false"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<com.github.apognu.otter.views.DisableableFrameLayout
|
||||||
android:id="@+id/container"
|
android:id="@+id/container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<FrameLayout
|
<com.github.apognu.otter.views.DisableableFrameLayout
|
||||||
android:id="@+id/landscape_queue"
|
android:id="@+id/landscape_queue"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
style="@style/AppTheme.Title"
|
style="@style/AppTheme.Title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -7,6 +7,7 @@ buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:4.0.0")
|
classpath("com.android.tools.build:gradle:4.0.0")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
|
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
|
kotlin.code.style=official
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue