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