diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ade2a7a..2b18952 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,6 +127,11 @@ dependencies { 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("org.koin:koin-core:2.1.6") + implementation("org.koin:koin-android:2.1.6") + implementation("org.koin:koin-androidx-scope:2.1.6") + implementation("org.koin:koin-androidx-viewmodel:2.1.6") + implementation("org.koin:koin-androidx-fragment:2.1.6") implementation("androidx.fragment:fragment-ktx:1.2.5") implementation("androidx.room:room-runtime:2.2.5") implementation("androidx.room:room-ktx:2.2.5") @@ -148,7 +153,6 @@ dependencies { 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") 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 ece32c0..a3c6cbd 100644 --- a/app/src/main/java/com/github/apognu/otter/Otter.kt +++ b/app/src/main/java/com/github/apognu/otter/Otter.kt @@ -1,15 +1,22 @@ package com.github.apognu.otter import android.app.Application +import android.content.Context import androidx.appcompat.app.AppCompatDelegate import androidx.room.Room +import com.github.apognu.otter.activities.MainActivity +import com.github.apognu.otter.activities.SearchActivity +import com.github.apognu.otter.adapters.* +import com.github.apognu.otter.fragments.* 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.playback.QueueManager.Companion.factory +import com.github.apognu.otter.repositories.* 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.github.apognu.otter.viewmodels.* import com.google.android.exoplayer2.database.ExoDatabaseProvider import com.google.android.exoplayer2.offline.DefaultDownloadIndex import com.google.android.exoplayer2.offline.DefaultDownloaderFactory @@ -20,7 +27,15 @@ 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.CoroutineScope import kotlinx.coroutines.channels.BroadcastChannel +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.androidx.fragment.dsl.fragment +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.context.startKoin +import org.koin.core.parameter.parametersOf +import org.koin.dsl.module import java.text.SimpleDateFormat import java.util.* @@ -36,12 +51,6 @@ class Otter : Application() { val eventBus: BroadcastChannel = BroadcastChannel(10) val commandBus: BroadcastChannel = BroadcastChannel(10) - val database: OtterDatabase by lazy { - Room - .databaseBuilder(this, OtterDatabase::class.java, "otter") - .build() - } - private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) } val exoCache: SimpleCache by lazy { @@ -65,7 +74,7 @@ class Otter : Application() { } val exoDownloadManager: DownloadManager by lazy { - DownloaderConstructorHelper(exoDownloadCache, QueueManager.factory(this)).run { + DownloaderConstructorHelper(exoDownloadCache, factory(this)).run { DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this)) } } @@ -83,6 +92,62 @@ class Otter : Application() { instance = this + startKoin { + androidLogger() + androidContext(this@Otter) + + modules(module { + single { + synchronized(this) { + Room + .databaseBuilder(get(), OtterDatabase::class.java, "otter") + .build() + } + } + + factory { MainActivity() } + factory { SearchActivity(get(), get()) } + + fragment { BrowseFragment() } + fragment { LandscapeQueueFragment() } + + single { PlayerStateViewModel(get()) } + + single { ArtistsRepository(get(), get()) } + factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) } + viewModel { ArtistsViewModel(get()) } + factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) } + + factory { (id: Int?) -> AlbumsRepository(get(), get(), id) } + viewModel { (id: Int?) -> AlbumsViewModel(get { parametersOf(id) }, get { parametersOf(id) }, id) } + factory { (context: Context?, adapter: AlbumsAdapter.OnAlbumClickListener) -> AlbumsAdapter(context, adapter) } + factory { (context: Context?, adapter: AlbumsGridAdapter.OnAlbumClickListener) -> AlbumsGridAdapter(context, adapter) } + + factory { (id: Int?) -> TracksRepository(get(), get(), id) } + viewModel { (id: Int) -> TracksViewModel(get { parametersOf(id) }, get(), id) } + factory { (context: Context?, favoriteListener: TracksAdapter.OnFavoriteListener?) -> TracksAdapter(context, favoriteListener) } + + single { PlaylistsRepository(get(), get()) } + factory { (id: Int) -> PlaylistTracksRepository(get(), get(), id) } + viewModel { PlaylistsViewModel(get()) } + viewModel { (id: Int) -> PlaylistViewModel(get { parametersOf(id) }, get { parametersOf(null) }, get(), id) } + factory { (context: Context?, listener: PlaylistsAdapter.OnPlaylistClickListener) -> PlaylistsAdapter(context, listener) } + factory { (context: Context?, listener: PlaylistTracksAdapter.OnFavoriteListener) -> PlaylistTracksAdapter(context, listener) } + + single { FavoritesRepository(get(), get()) } + single { FavoritedRepository(get(), get()) } + factory { (context: Context?, listener: FavoritesAdapter.OnFavoriteListener) -> FavoritesAdapter(context, listener) } + viewModel { FavoritesViewModel(get(), get { parametersOf(null) }) } + + single { RadiosRepository(get(), get()) } + factory { (context: Context?, scope: CoroutineScope, listener: RadiosAdapter.OnRadioClickListener) -> RadiosAdapter(context, scope, listener) } + viewModel { RadiosViewModel(get()) } + + single { (scope: CoroutineScope) -> QueueRepository(get(), scope) } + viewModel { QueueViewModel(get(), get()) } + }) + } + when (PowerPreference.getDefaultFile().getString("night_mode")) { "on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "off" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 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 f999c51..03ac278 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 @@ -14,11 +14,9 @@ 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.result.Result -import com.google.gson.Gson import com.preference.PowerPreference import kotlinx.android.synthetic.main.activity_login.* import kotlinx.coroutines.Dispatchers.Main @@ -131,12 +129,12 @@ class LoginActivity : AppCompatActivity() { is Result.Failure -> { dialog.dismiss() - val error = Gson().fromJson(String(response.data), Credentials::class.java) + val error = AppContext.json.parse(Credentials.serializer(), String(response.data)) hostname_field.error = null username_field.error = null - if (error != null && error.non_field_errors?.isNotEmpty() == true) { + if (error.non_field_errors?.isNotEmpty() == true) { username_field.error = error.non_field_errors[0] } else { hostname_field.error = result.error.localizedMessage 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 6d20850..0f35327 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 @@ -18,11 +18,10 @@ import androidx.fragment.app.DialogFragment 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.Otter 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 @@ -31,16 +30,12 @@ import com.github.apognu.otter.repositories.FavoritedRepository import com.github.apognu.otter.repositories.FavoritesRepository 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 import com.google.android.exoplayer2.Player 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.* @@ -50,17 +45,20 @@ import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.stringify +import org.koin.android.ext.android.inject class MainActivity : AppCompatActivity() { + private val playerViewModel by inject() + private val favoritesRepository by inject() + private val favoritedRepository by inject() + private val browseFragment by inject() + private var menu: Menu? = null + enum class ResultCode(val code: Int) { LOGOUT(1001) } - private val queueViewModel = QueueViewModel.get() - private val favoritesRepository = FavoritesRepository(this) - private val favoritedRepository = FavoritedRepository(this) - private var menu: Menu? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -70,17 +68,17 @@ class MainActivity : AppCompatActivity() { setSupportActionBar(appbar) when (intent.action) { - MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment()) + MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(inject().value) } supportFragmentManager .beginTransaction() - .replace(R.id.container, BrowseFragment()) + .replace(R.id.container, browseFragment) .commit() watchEventBus() - PlayerStateViewModel.get().isPlaying.observe(this) { isPlaying -> + playerViewModel.isPlaying.observe(this) { isPlaying -> when (isPlaying) { true -> { now_playing_toggle.icon = getDrawable(R.drawable.pause) @@ -94,18 +92,18 @@ class MainActivity : AppCompatActivity() { } } - PlayerStateViewModel.get().isBuffering.observe(this) { isBuffering -> + playerViewModel.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 -> + playerViewModel.track.observe(this) { track -> refreshCurrentTrack(track) } - PlayerStateViewModel.get().position.observe(this) { (current, duration, percent) -> + playerViewModel.position.observe(this) { (current, duration, percent) -> now_playing_progress.progress = percent now_playing_details_progress.progress = percent @@ -187,7 +185,7 @@ class MainActivity : AppCompatActivity() { }) landscape_queue?.let { - supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, LandscapeQueueFragment()).commit() + supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, inject().value).commit() } } @@ -231,7 +229,7 @@ class MainActivity : AppCompatActivity() { return true } - launchFragment(BrowseFragment()) + launchFragment(browseFragment) } R.id.nav_queue -> launchDialog(QueueFragment()) @@ -573,7 +571,7 @@ class MainActivity : AppCompatActivity() { .post(mustNormalizeUrl("/api/v1/history/listenings/")) .authorize() .header("Content-Type", "application/json") - .body(Gson().toJson(mapOf("track" to track.id))) + .body(AppContext.json.stringify(mapOf("track" to track.id))) .awaitStringResponse() } catch (_: Exception) { } 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 3dc94f5..f07b938 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,36 +5,39 @@ 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.OtterDatabase 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.github.apognu.otter.repositories.AlbumsSearchRepository +import com.github.apognu.otter.repositories.ArtistsSearchRepository +import com.github.apognu.otter.repositories.FavoritesRepository +import com.github.apognu.otter.repositories.TracksSearchRepository +import com.github.apognu.otter.utils.Event +import com.github.apognu.otter.utils.EventBus +import com.github.apognu.otter.utils.untilNetwork 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 org.koin.android.ext.android.inject import java.net.URLEncoder import java.util.* -class SearchActivity : AppCompatActivity() { +class SearchActivity(private val database: OtterDatabase, private val favoritesRepository: FavoritesRepository) : AppCompatActivity() { private lateinit var adapter: SearchAdapter lateinit var artistsRepository: ArtistsSearchRepository lateinit var albumsRepository: AlbumsSearchRepository lateinit var tracksRepository: TracksSearchRepository - lateinit var favoritesRepository: FavoritesRepository - var done = 0 override fun onCreate(savedInstanceState: Bundle?) { @@ -64,7 +67,6 @@ class SearchActivity : AppCompatActivity() { artistsRepository = ArtistsSearchRepository(this@SearchActivity, "") albumsRepository = AlbumsSearchRepository(this@SearchActivity, "") tracksRepository = TracksSearchRepository(this@SearchActivity, "") - favoritesRepository = FavoritesRepository(this@SearchActivity) search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { override fun onQueryTextSubmit(rawQuery: String?): Boolean { @@ -92,7 +94,7 @@ class SearchActivity : AppCompatActivity() { done++ artists.forEach { - Otter.get().database.artists().run { + database.artists().run { insert(it.toDao()) adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id))) @@ -108,7 +110,7 @@ class SearchActivity : AppCompatActivity() { done++ albums.forEach { - Otter.get().database.albums().run { + database.albums().run { insert(it.toDao()) adapter.albums.add(Album.fromDecoratedEntity(getDecoratedBlocking(it.id))) @@ -124,8 +126,8 @@ class SearchActivity : AppCompatActivity() { done++ tracks.forEach { - Otter.get().database.tracks().run { - insertWithAssocs(it) + database.tracks().run { + insertWithAssocs(database.artists(), database.albums(), database.uploads(), it) adapter.tracks.add(Track.fromDecoratedEntity(getDecoratedBlocking(it.id))) } 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 01dfee9..086b441 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 @@ -41,10 +41,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL .into(holder.art) holder.name.text = artist.name - - context?.let { - holder.albums.text = context.resources.getQuantityString(R.plurals.album_count, artist.album_count, artist.album_count) - } + holder.albums.text = context?.resources?.getQuantityString(R.plurals.album_count, artist.album_count, artist.album_count) ?: "" } inner class ViewHolder(view: View, private val listener: OnArtistClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { 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 fb81b1f..714c872 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 @@ -3,17 +3,17 @@ package com.github.apognu.otter.adapters import android.annotation.SuppressLint import android.content.Context import android.graphics.Color -import android.graphics.Typeface +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.graphics.drawable.ColorDrawable -import android.os.Build import android.view.* import androidx.appcompat.widget.PopupMenu 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.utils.* 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 import kotlinx.android.synthetic.main.row_track.view.* @@ -26,8 +26,6 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL private lateinit var touchHelper: ItemTouchHelper - var currentTrack: Track? = null - override fun getItemCount() = data.size override fun getItemId(position: Int): Long { @@ -68,15 +66,11 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL context?.let { holder.itemView.background = context.getDrawable(R.drawable.ripple) - } - if (track == currentTrack) { - context?.let { + if (track.current) { holder.itemView.background = context.getDrawable(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)) @@ -85,6 +79,23 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL holder.favorite.setOnClickListener { favoriteListener?.onToggleFavorite(track.id, !track.favorite) } + + when (track.cached || track.downloaded) { + true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0) + false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) + } + + if (track.cached && !track.downloaded) { + holder.title.compoundDrawables.forEach { + it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN) + } + } + + if (track.downloaded) { + holder.title.compoundDrawables.forEach { + it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN) + } + } } holder.actions.setOnClickListener { @@ -96,7 +107,7 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL when (it.itemId) { 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)) + R.id.track_pin -> CommandBus.send(Command.PinTrack(track)) } true 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 531d58a..dadde66 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,8 +6,8 @@ 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.models.dao.RadioEntity import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.Event import com.github.apognu.otter.utils.EventBus 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 9f2152c..d96ebf3 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 @@ -57,6 +57,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener: .maybeLoad(maybeNormalizeUrl(track.album?.cover())) .fit() .transform(RoundedCornersTransformation(8, 0)) + .placeholder(R.drawable.cover) .into(holder.cover) holder.title.text = track.title 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 aaae0a4..39ec1ac 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,7 +8,6 @@ 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 @@ -18,27 +17,34 @@ 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.models.domain.Album +import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.repositories.AlbumsRepository import com.github.apognu.otter.repositories.ArtistTracksRepository 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.launch +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class AlbumsFragment : LiveOtterFragment() { - override lateinit var liveData: LiveData> + override val repository by inject { parametersOf(null) } + override val adapter by inject { parametersOf(context, OnAlbumClickListener()) } + override val viewModel by viewModel { parametersOf(artistId) } + override val liveData by lazy { viewModel.albums } + override val viewRes = R.layout.fragment_albums override val recycler: RecyclerView get() = albums override val alwaysRefresh = false - private lateinit var artistTracksRepository: ArtistTracksRepository + private val artistTracksRepository by inject { parametersOf(artistId) } - var artistId = 0 + private var artistId = 0 var artistName = "" var artistArt = "" @@ -93,19 +99,13 @@ class AlbumsFragment : LiveOtterFragment() } 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) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -142,7 +142,7 @@ class AlbumsFragment : LiveOtterFragment() play.isClickable = true lifecycleScope.launch(IO) { - AlbumsViewModel(artistId).tracks().also { + viewModel.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 c35d863..7f6629a 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 @@ -1,6 +1,5 @@ package com.github.apognu.otter.fragments -import android.os.Bundle import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import androidx.recyclerview.widget.GridLayoutManager @@ -11,26 +10,26 @@ 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.models.domain.Album import com.github.apognu.otter.repositories.AlbumsRepository import com.github.apognu.otter.utils.AppContext -import com.github.apognu.otter.models.domain.Album import com.github.apognu.otter.viewmodels.AlbumsViewModel import kotlinx.android.synthetic.main.fragment_albums_grid.* +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class AlbumsGridFragment : LiveOtterFragment() { - override val liveData = AlbumsViewModel().albums + override val repository by inject { parametersOf(null) } + override val adapter by inject { parametersOf(context, OnAlbumClickListener()) } + override val viewModel by viewModel { parametersOf(null) } + override val liveData by lazy { viewModel.albums } + override val viewRes = R.layout.fragment_albums_grid override val recycler: RecyclerView get() = albums override val layoutManager get() = GridLayoutManager(context, 3) override val alwaysRefresh = false - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - adapter = AlbumsGridAdapter(context, OnAlbumClickListener()) - repository = AlbumsRepository(context) - } - inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener { override fun onClick(view: View?, album: Album) { (context as? MainActivity)?.let { activity -> 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 c3fe046..980a998 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 @@ -18,15 +18,22 @@ 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.models.domain.Artist import com.github.apognu.otter.repositories.ArtistsRepository import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.onViewPager -import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.viewmodels.ArtistsViewModel import kotlinx.android.synthetic.main.fragment_artists.* +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class ArtistsFragment : LiveOtterFragment() { - override val liveData = ArtistsViewModel.get().artists + override val repository by inject() + override val adapter by inject { parametersOf(context, OnArtistClickListener()) } + override val viewModel by viewModel() + + override val liveData by lazy { viewModel.artists } override val viewRes = R.layout.fragment_artists override val recycler: RecyclerView get() = artists @@ -66,13 +73,6 @@ class ArtistsFragment : LiveOtterFragment() { - override val liveData = TracksViewModel(0).favorites + override val repository by inject() + override val adapter by inject { parametersOf(context, FavoriteListener()) } + override val viewModel by viewModel() + override val liveData by lazy { viewModel.favorites } + override val viewRes = R.layout.fragment_favorites override val recycler: RecyclerView get() = favorites override val alwaysRefresh = false + private val playerViewModel by inject() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - adapter = FavoritesAdapter(context, FavoriteListener()) - repository = FavoritesRepository(context) - - PlayerStateViewModel.get().track.observe(this) { refreshCurrentTrack(it) } + playerViewModel.track.observe(this) { refreshCurrentTrack(it) } watchEventBus() } 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 e019822..ff049d7 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 @@ -14,19 +14,15 @@ 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 org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel class LandscapeQueueFragment : Fragment() { + private val viewModel by viewModel() + private val favoritesRepository by inject() + private var adapter: TracksAdapter? = null - private val viewModel = QueueViewModel.get() - lateinit var favoritesRepository: FavoritesRepository - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - favoritesRepository = FavoritesRepository(context) - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { viewModel.queue.observe(viewLifecycleOwner) { refresh(it) diff --git a/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt index 13d4f0e..0bb3245 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import androidx.lifecycle.observe import androidx.recyclerview.widget.LinearLayoutManager @@ -34,20 +35,22 @@ abstract class OtterAdapter : RecyclerView.Adap abstract override fun getItemId(position: Int): Long } -abstract class LiveOtterFragment> : Fragment() { +abstract class LiveOtterFragment> : Fragment() { companion object { const val OFFSCREEN_PAGES = 20 } - abstract val liveData: LiveData> + abstract val repository: Repository + abstract val adapter: A + open val viewModel: ViewModel? = null + + 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 adapter: A - private var moreLoading = false private var listener: Job? = null @@ -58,6 +61,13 @@ abstract class LiveOtterFragment> : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + liveData.observe(viewLifecycleOwner) { + onDataUpdated(it) + + adapter.data = it.toMutableList() + adapter.notifyDataSetChanged() + } + recycler.layoutManager = layoutManager (recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false recycler.adapter = adapter @@ -88,17 +98,6 @@ abstract class LiveOtterFragment> : } } } - - fetch() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - liveData.observe(this) { - adapter.data = it.toMutableList() - adapter.notifyDataSetChanged() - } } override fun onResume() { @@ -109,7 +108,8 @@ abstract class LiveOtterFragment> : } } - open fun onDataFetched(data: List) {} + open fun onDataFetched(data: List) {} + open fun onDataUpdated(data: List?) {} private fun fetch(size: Int = 0) { moreLoading = true 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 1857049..66fb32b 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,38 +5,41 @@ 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.models.domain.Track 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.launch +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class PlaylistTracksFragment : LiveOtterFragment() { - override lateinit var liveData: LiveData> + private val favoritesRepository by inject() + override val repository by inject { parametersOf(playlistId) } + override val adapter by inject { parametersOf(context, FavoriteListener()) } + override val viewModel by viewModel { parametersOf(playlistId) } + override val liveData by lazy { viewModel.tracks } + override val viewRes = R.layout.fragment_tracks override val recycler: RecyclerView get() = tracks - lateinit var favoritesRepository: FavoritesRepository - var playlistId = 0 var playlistName = "" companion object { - fun new(playlist: PlaylistEntity): PlaylistTracksFragment { + fun new(playlist: PlaylistEntity, favoritesRepository: FavoritesRepository): PlaylistTracksFragment { return PlaylistTracksFragment().apply { arguments = bundleOf( "playlistId" to playlist.id, @@ -47,23 +50,12 @@ class PlaylistTracksFragment : LiveOtterFragment - adapter.currentTrack = track - adapter.notifyDataSetChanged() - } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 7c0b929..3bcf07e 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 @@ -1,6 +1,5 @@ package com.github.apognu.otter.fragments -import android.os.Bundle import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import androidx.recyclerview.widget.RecyclerView @@ -11,23 +10,26 @@ 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.FavoritesRepository import com.github.apognu.otter.repositories.PlaylistsRepository import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.viewmodels.PlaylistsViewModel import kotlinx.android.synthetic.main.fragment_playlists.* +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class PlaylistsFragment : LiveOtterFragment() { - override val liveData = PlaylistsViewModel().playlists + override val repository by inject() + override val adapter by inject { parametersOf(context, OnPlaylistClickListener()) } + override val viewModel by viewModel() + override val liveData by lazy { viewModel.playlists } + override val viewRes = R.layout.fragment_playlists override val recycler: RecyclerView get() = playlists override val alwaysRefresh = false - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - adapter = PlaylistsAdapter(context, OnPlaylistClickListener()) - repository = PlaylistsRepository(context) - } + private val favoritesRepository by inject() inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener { override fun onClick(holder: View?, playlist: PlaylistEntity) { @@ -41,7 +43,7 @@ class PlaylistsFragment : LiveOtterFragment() + private val favoritesRepository by inject() - private val viewModel = QueueViewModel.get() - lateinit var favoritesRepository: FavoritesRepository + private var adapter: TracksAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - favoritesRepository = FavoritesRepository(context) - setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet) } 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 70bc1ce..82bbf77 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 @@ -1,6 +1,5 @@ package com.github.apognu.otter.fragments -import android.os.Bundle import androidx.core.view.forEach import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView @@ -15,20 +14,19 @@ import kotlinx.android.synthetic.main.fragment_radios.* import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.koin.core.parameter.parametersOf class RadiosFragment : LiveOtterFragment() { - override val liveData = RadiosViewModel().radios + override val repository by inject() + override val adapter by inject { parametersOf(context, lifecycleScope, RadioClickListener()) } + override val viewModel by inject() + override val liveData by lazy { viewModel.radios } + override val viewRes = R.layout.fragment_radios override val recycler: RecyclerView get() = radios override val alwaysRefresh = false - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - adapter = RadiosAdapter(context, lifecycleScope, RadioClickListener()) - repository = RadiosRepository(context) - } - inner class RadioClickListener : RadiosAdapter.OnRadioClickListener { override fun onClick(holder: RadiosAdapter.ViewHolder, radio: RadioEntity) { holder.spin() @@ -37,7 +35,6 @@ class RadiosFragment : LiveOtterFragment() { - override lateinit var liveData: LiveData> + override val repository by inject { parametersOf(albumId) } + override val adapter by inject { parametersOf(context, FavoriteListener()) } + override val viewModel by viewModel { parametersOf(albumId) } + override val liveData by lazy { viewModel.tracks } + override val viewRes = R.layout.fragment_tracks override val recycler: RecyclerView get() = tracks - lateinit var favoritesRepository: FavoritesRepository - lateinit var favoritedRepository: FavoritedRepository + private val favoritesRepository by inject() private var albumId = 0 - private var albumArtist = "" - private var albumTitle = "" - private var albumCover = "" companion object { fun new(album: Album): TracksFragment { @@ -53,35 +54,12 @@ class TracksFragment : LiveOtterFragment() } override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.apply { albumId = getInt("albumId") } - 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) - favoritedRepository = FavoritedRepository(context) - watchEventBus() } @@ -156,6 +134,21 @@ class TracksFragment : LiveOtterFragment() } } + override fun onDataUpdated(data: List?) { + data?.let { + title.text = data.getOrNull(0)?.album?.title + artist.text = data.getOrNull(0)?.artist?.name + + Picasso.get() + .maybeLoad(data.getOrNull(0)?.album?.cover) + .noFade() + .fit() + .centerCrop() + .transform(RoundedCornersTransformation(16, 0)) + .into(cover) + } + } + private fun watchEventBus() { lifecycleScope.launch(IO) { EventBus.get().collect { message -> 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 index 6cf1215..126b66f 100644 --- 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 @@ -21,7 +21,7 @@ class OtterResponseSerializer(private val dataSerializer: KSerializer? = null) +data class Credentials(val token: String? = null, 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/Track.kt b/app/src/main/java/com/github/apognu/otter/models/api/Track.kt index 5c3f6f1..38efaeb 100644 --- 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 @@ -2,6 +2,7 @@ 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.ContextualSerialization import kotlinx.serialization.Serializable @Serializable @@ -55,10 +56,12 @@ data class FunkwhaleTrack( @Serializable data class Favorited(val track: Int) +@Serializable data class DownloadInfo( val id: Int, val contentId: String, val title: String, val artist: String, + @ContextualSerialization var download: Download? ) \ 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 index cdd9ca8..10fd465 100644 --- 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 @@ -5,6 +5,7 @@ import androidx.room.* import androidx.room.ForeignKey.CASCADE import com.github.apognu.otter.Otter import com.github.apognu.otter.models.api.FunkwhaleTrack +import org.koin.java.KoinJavaComponent.inject @Entity(tableName = "tracks") data class TrackEntity( @@ -59,17 +60,17 @@ data class TrackEntity( fun insert(track: TrackEntity) @Transaction - fun insertWithAssocs(track: FunkwhaleTrack) { - Otter.get().database.artists().insert(track.artist.toDao()) + fun insertWithAssocs(artistsDao: ArtistEntity.Dao, albumsDao: AlbumEntity.Dao, uploadsDao: UploadEntity.Dao, track: FunkwhaleTrack) { + artistsDao.insert(track.artist.toDao()) track.album?.let { - Otter.get().database.albums().insert(it.toDao()) + albumsDao.insert(it.toDao()) } insert(track.toDao()) track.uploads.forEach { - Otter.get().database.uploads().insert(it.toDao(track.id)) + uploadsDao.insert(it.toDao(track.id)) } } } @@ -103,6 +104,7 @@ fun FunkwhaleTrack.toDao() = run { ON ar.id = al.artist_id LEFT JOIN favorites ON favorites.track_id = tracks.id + ORDER BY tracks.position """) data class DecoratedTrackEntity( val id: Int, 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 2f5b2f6..bcef9aa 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 @@ -7,29 +7,27 @@ 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.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.mustNormalizeUrl +import com.github.apognu.otter.viewmodels.DownloadsViewModel import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.DownloadManager import com.google.android.exoplayer2.offline.DownloadRequest import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.scheduler.Scheduler import com.google.android.exoplayer2.ui.DownloadNotificationHelper -import com.google.gson.Gson -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.Job +import kotlinx.serialization.stringify import java.util.* class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) { - private val scope: CoroutineScope = CoroutineScope(Job() + Main) - companion object { fun download(context: Context, track: Track) { track.bestUpload()?.let { upload -> val url = mustNormalizeUrl(upload.listen_url) - val data = Gson().toJson( + val data = AppContext.json.stringify( DownloadInfo( track.id, url, 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 5f75a5c..f31113a 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,6 @@ 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 @@ -32,8 +31,11 @@ import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.flow.collect +import org.koin.android.ext.android.inject class PlayerService : Service() { + private val playerViewModel by inject() + companion object { const val INITIAL_COMMAND_KEY = "start_command" } @@ -136,7 +138,7 @@ class PlayerService : Service() { val (current, duration, percent) = getProgress(true) - PlayerStateViewModel.get().position.postValue(Triple(current, duration, percent)) + playerViewModel.position.postValue(Triple(current, duration, percent)) } } @@ -149,8 +151,8 @@ class PlayerService : Service() { when (command) { is Command.RefreshService -> { if (queue.metadata.isNotEmpty()) { - PlayerStateViewModel.get()._track.postValue(queue.current()) - PlayerStateViewModel.get().isPlaying.postValue(player.playWhenReady) + playerViewModel._track.postValue(queue.current()) + playerViewModel.isPlaying.postValue(player.playWhenReady) } } @@ -211,7 +213,7 @@ class PlayerService : Service() { delay(1000) if (player.playWhenReady) { - PlayerStateViewModel.get().position.postValue(getProgress()) + playerViewModel.position.postValue(getProgress()) } } } @@ -271,7 +273,7 @@ class PlayerService : Service() { if (hasAudioFocus(state)) { player.playWhenReady = state - PlayerStateViewModel.get().isPlaying.postValue(state) + playerViewModel.isPlaying.postValue(state) } } @@ -291,7 +293,7 @@ class PlayerService : Service() { player.next() Cache.set(this@PlayerService, "progress", "0".toByteArray()) - PlayerStateViewModel.get().position.postValue(Triple(0, 0, 0)) + playerViewModel.position.postValue(Triple(0, 0, 0)) } private fun getProgress(force: Boolean = false): Triple { @@ -371,17 +373,17 @@ class PlayerService : Service() { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { super.onPlayerStateChanged(playWhenReady, playbackState) - PlayerStateViewModel.get().isPlaying.postValue(playWhenReady) + playerViewModel.isPlaying.postValue(playWhenReady) if (queue.current == -1) { - PlayerStateViewModel.get()._track.postValue(queue.current()) + playerViewModel._track.postValue(queue.current()) } when (playWhenReady) { true -> { when (playbackState) { Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true) - Player.STATE_BUFFERING -> PlayerStateViewModel.get().isBuffering.postValue(true) + Player.STATE_BUFFERING -> playerViewModel.isBuffering.postValue(true) Player.STATE_ENDED -> { setPlaybackState(false) @@ -396,11 +398,11 @@ class PlayerService : Service() { } } - if (playbackState != Player.STATE_BUFFERING) PlayerStateViewModel.get().isBuffering.postValue(false) + if (playbackState != Player.STATE_BUFFERING) playerViewModel.isBuffering.postValue(false) } false -> { - PlayerStateViewModel.get().isBuffering.postValue(false) + playerViewModel.isBuffering.postValue(false) Build.VERSION_CODES.N.onApi( { stopForeground(STOP_FOREGROUND_DETACH) }, @@ -434,7 +436,7 @@ class PlayerService : Service() { Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray()) - PlayerStateViewModel.get()._track.postValue(queue.current()) + playerViewModel._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 3fa0027..a7bffb9 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 @@ -18,9 +18,13 @@ import com.google.android.exoplayer2.util.Util import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import org.koin.core.KoinComponent +import org.koin.core.inject +import org.koin.core.parameter.parametersOf -class QueueManager(val context: Context) { - private val queueRepository = QueueRepository(GlobalScope) +class QueueManager(val context: Context) : KoinComponent { + private val playerViewModel by inject() + private val queueRepository by inject { parametersOf(GlobalScope) } var metadata: MutableList = mutableListOf() val datasources = ConcatenatingMediaSource() @@ -59,7 +63,7 @@ class QueueManager(val context: Context) { Cache.get(context, "current")?.let { string -> current = string.readLine().toInt() - PlayerStateViewModel.get()._track.postValue(current()) + playerViewModel._track.postValue(current()) } } 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 3c86128..8981476 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,17 +1,15 @@ 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.OtterDatabase 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.utils.* import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResult -import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -19,6 +17,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable +import kotlinx.serialization.stringify +import org.koin.core.KoinComponent +import org.koin.core.inject @Serializable data class RadioSessionBody(val radio_type: String?, var custom_radio: Int? = null, var related_object_id: String? = null) @@ -35,15 +36,15 @@ data class RadioTrack(val position: Int, val track: RadioTrackID) @Serializable data class RadioTrackID(val id: Int) -class RadioPlayer(val context: Context, val scope: CoroutineScope) { +class RadioPlayer(val context: Context, val scope: CoroutineScope) : KoinComponent { + private val database by inject() + val lock = Semaphore(1) private var currentRadio: RadioEntity? = null private var session: Int? = null private var cookie: String? = null - private val favoritedRepository = FavoritedRepository(context) - init { Cache.get(context, "radio_type")?.readLine()?.let { radio_type -> Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id -> @@ -80,8 +81,6 @@ 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 { @@ -90,7 +89,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { } } - val body = Gson().toJson(request) + val body = AppContext.json.stringify(request) val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/")) .authorize() .header("Content-Type", "application/json") @@ -107,8 +106,6 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { prepareNextTrack(true) } catch (e: Exception) { - e.log() - withContext(Main) { context.toast(context.getString(R.string.radio_playback_error)) } @@ -117,11 +114,9 @@ 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)) + val body = AppContext.json.stringify(RadioTrackBody(session)) val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/")) .authorize() .header("Content-Type", "application/json") @@ -138,8 +133,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { .awaitObjectResult(AppContext.deserializer()) .get() - Otter.get().database.tracks().run { - insertWithAssocs(track) + database.tracks().run { + insertWithAssocs(database.artists(), database.albums(), database.uploads(), track) Track.fromDecoratedEntity(find(track.id)).let { track -> if (first) { 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 8d1ed11..b0ad13b 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,11 +1,16 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData import com.github.apognu.otter.models.api.FunkwhaleAlbum +import com.github.apognu.otter.models.dao.DecoratedAlbumEntity +import com.github.apognu.otter.models.dao.OtterDatabase import com.github.apognu.otter.models.dao.toDao +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch -class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository() { +class AlbumsRepository(override val context: Context, private val database: OtterDatabase, artistId: Int?) : Repository() { override val upstream: Upstream by lazy { val url = if (artistId == null) "/api/v1/albums/?playable=true&ordering=title" @@ -20,9 +25,19 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) : override fun onDataFetched(data: List): List { data.forEach { - Otter.get().database.albums().insert(it.toDao()) + database.albums().insert(it.toDao()) } return super.onDataFetched(data) } + + fun all() = database.albums().allDecorated() + + fun ofArtist(id: Int): LiveData> { + scope.launch(Dispatchers.IO) { + fetch().collect() + } + + return database.albums().forArtistDecorated(id) + } } \ 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 8a952b2..7438735 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,17 +1,17 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter import com.github.apognu.otter.models.api.FunkwhaleTrack +import com.github.apognu.otter.models.dao.OtterDatabase import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.runBlocking -class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository() { +class ArtistTracksRepository(override val context: Context, private val database: OtterDatabase, artistId: Int) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", FunkwhaleTrack.serializer()) override fun onDataFetched(data: List) = runBlocking(IO) { data.forEach { - Otter.get().database.tracks().insertWithAssocs(it) + database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it) } super.onDataFetched(data) 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 e657d8c..42a2efe 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,33 +1,45 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData import com.github.apognu.otter.models.api.FunkwhaleArtist +import com.github.apognu.otter.models.dao.DecoratedArtistEntity +import com.github.apognu.otter.models.dao.OtterDatabase 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.flow.collect import kotlinx.coroutines.launch -class ArtistsRepository(override val context: Context?) : Repository() { +class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer()) override fun onDataFetched(data: List): List { scope.launch(IO) { data.forEach { artist -> - Otter.get().database.artists().insert(artist.toDao()) + 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)) + database.albums().insert(album.toDao(artist.id)) } } } return super.onDataFetched(data) } + + fun all(): LiveData> { + scope.launch(IO) { + fetch().collect() + } + + return database.artists().allDecorated() + } + fun get(id: Int) = database.artists().getDecorated(id) } \ 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 9e0ad0c..9fcd477 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,30 +1,32 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData 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.models.dao.OtterDatabase +import com.github.apognu.otter.utils.AppContext 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.google.gson.Gson import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.serialization.stringify -class FavoritesRepository(override val context: Context?) : Repository() { +class FavoritesRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", FunkwhaleTrack.serializer()) - val favoritedRepository = FavoritedRepository(context) + val favoritedRepository = FavoritedRepository(context, database) override fun onDataFetched(data: List): List = runBlocking { data.forEach { - Otter.get().database.tracks().insertWithAssocs(it) - Otter.get().database.favorites().insert(FavoriteEntity(it.id)) + database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it) + database.favorites().insert(FavoriteEntity(it.id)) } /* val downloaded = TracksRepository.getDownloadedIds() ?: listOf() @@ -45,8 +47,18 @@ class FavoritesRepository(override val context: Context?) : Repository> { + scope.launch(IO) { + fetch().collect() + } + + return database.favorites().all() + } + + fun find(ids: List) = database.albums().findAllDecorated(ids) + fun addFavorite(id: Int) = scope.launch(IO) { - Otter.get().database.favorites().add(id) + database.favorites().add(id) val body = mapOf("track" to id) @@ -59,7 +71,7 @@ class FavoritesRepository(override val context: Context?) : Repository() { +class FavoritedRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer()) override fun onDataFetched(data: List): List { scope.launch(IO) { data.forEach { - Otter.get().database.favorites().insert(FavoriteEntity(it.track)) + database.favorites().insert(FavoriteEntity(it.track)) } } 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 a9b2111..4d7b304 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 @@ -52,9 +52,6 @@ class HttpUpstream(val behavior: Behavior, private val url: String, pri } }, { error -> - "GET $url".log() - error.log() - when (error.exception) { is RefreshError -> EventBus.send(Event.LogOut) else -> send(Repository.Response(listOf(), page, false)) 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 d2f340a..bf1ba4c 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,24 +1,35 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack +import com.github.apognu.otter.models.dao.DecoratedTrackEntity +import com.github.apognu.otter.models.dao.OtterDatabase import com.github.apognu.otter.models.dao.PlaylistTrack -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -class PlaylistTracksRepository(override val context: Context?, private val playlistId: Int) : Repository() { +class PlaylistTracksRepository(override val context: Context?, private val database: OtterDatabase, private val playlistId: Int) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer()) override fun onDataFetched(data: List): List = runBlocking { - Otter.get().database.playlists().replaceTracks(playlistId, data.map { - Otter.get().database.tracks().insertWithAssocs(it.track) + database.playlists().replaceTracks(playlistId, data.map { + database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it.track) PlaylistTrack(playlistId, it.track.id) }) data } + + fun tracks(id: Int): LiveData> { + scope.launch(Dispatchers.IO) { + fetch().collect() + } + + return database.playlists().tracksFor(id) + } } \ 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 1718cb2..2bc54ac 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,19 +1,41 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData import com.github.apognu.otter.models.api.FunkwhalePlaylist +import com.github.apognu.otter.models.dao.DecoratedTrackEntity +import com.github.apognu.otter.models.dao.OtterDatabase +import com.github.apognu.otter.models.dao.PlaylistEntity import com.github.apognu.otter.models.dao.toDao +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch -class PlaylistsRepository(override val context: Context?) : Repository() { +class PlaylistsRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer()) override fun onDataFetched(data: List): List { data.forEach { - Otter.get().database.playlists().insert(it.toDao()) + database.playlists().insert(it.toDao()) } return super.onDataFetched(data) } + + fun all(): LiveData> { + scope.launch(IO) { + fetch().collect() + } + + return database.playlists().all() + } + + fun tracks(id: Int): LiveData> { + scope.launch(IO) { + fetch().collect() + } + + return database.playlists().tracksFor(id) + } } \ 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 index 3b71dab..f2bf362 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/QueueRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/QueueRepository.kt @@ -1,16 +1,16 @@ package com.github.apognu.otter.repositories -import com.github.apognu.otter.Otter +import com.github.apognu.otter.models.dao.OtterDatabase 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() +class QueueRepository(private val database: OtterDatabase, private val scope: CoroutineScope) { + fun all() = database.queue().allDecorated() - fun allBlocking() = Otter.get().database.queue().allDecoratedBlocking() + fun allBlocking() = database.queue().allDecoratedBlocking() fun replace(tracks: List) = scope.launch { - Otter.get().database.queue().replace(tracks) + 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 da5b4f3..dbd469b 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,21 +1,32 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.Otter +import androidx.lifecycle.LiveData import com.github.apognu.otter.models.api.FunkwhaleRadio +import com.github.apognu.otter.models.dao.OtterDatabase +import com.github.apognu.otter.models.dao.RadioEntity import com.github.apognu.otter.models.dao.toDao +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch -class RadiosRepository(override val context: Context?) : Repository() { +class RadiosRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer()) override fun onDataFetched(data: List): List { data.forEach { - Otter.get().database.radios().insert(it.toDao()) + database.radios().insert(it.apply { radio_type = "custom" }.toDao()) } return data - .map { radio -> radio.apply { radio_type = "custom" } } - .toMutableList() + } + + fun all(): LiveData> { + scope.launch(Dispatchers.IO) { + fetch().collect() + } + + return database.radios().all() } } \ No newline at end of file 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 d4d881a..d21fc32 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 @@ -4,8 +4,6 @@ import android.content.Context 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 class TracksSearchRepository(override val context: Context?, var query: String) : Repository() { @@ -13,11 +11,6 @@ class TracksSearchRepository(override val context: Context?, var query: String) get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer()) override fun onDataFetched(data: List): List = runBlocking { - val favorites = FavoritedRepository(context).fetch() - .map { it.data } - .toList() - .flatten() - /* val downloaded = TracksRepository.getDownloadedIds() ?: listOf() data.map { track -> 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 3b0e088..e2a1381 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 @@ -1,13 +1,21 @@ package com.github.apognu.otter.repositories import android.content.Context +import androidx.lifecycle.LiveData import com.github.apognu.otter.Otter import com.github.apognu.otter.models.api.FunkwhaleTrack +import com.github.apognu.otter.models.dao.DecoratedTrackEntity +import com.github.apognu.otter.models.dao.OtterDatabase +import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.utils.getMetadata +import com.github.apognu.otter.utils.maybeNormalizeUrl import com.google.android.exoplayer2.offline.Download +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -class TracksRepository(override val context: Context?, albumId: Int) : Repository() { +class TracksRepository(override val context: Context, private val database: OtterDatabase, albumId: Int?) : Repository() { override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer()) @@ -32,9 +40,42 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor override fun onDataFetched(data: List): List = runBlocking { data.forEach { track -> - Otter.get().database.tracks().insertWithAssocs(track) + database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track) } data.sortedWith(compareBy({ it.disc_number }, { it.position })) } + + fun find(ids: List) = database.tracks().findAllDecorated(ids) + + suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id) + + fun ofAlbums(albums: List): LiveData> { + scope.launch(IO) { + fetch().collect() + } + + return database.tracks().ofAlbumsDecorated(albums) + } + + fun favorites() = database.tracks().favorites() + + fun isCached(track: Track): Boolean = Otter.get().exoCache.isCached(maybeNormalizeUrl(track.bestUpload()?.listen_url), 0L, track.bestUpload()?.duration?.toLong() ?: 0) + + fun downloaded(): List { + val cursor = Otter.get().exoDownloadManager.downloadIndex.getDownloads() + val ids: MutableList = mutableListOf() + + while (cursor.moveToNext()) { + val download = cursor.download + + download.getMetadata()?.let { + if (download.state == Download.STATE_COMPLETED) { + ids.add(it.id) + } + } + } + + return ids + } } \ 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 b82cc44..4c1b6b5 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 @@ -29,11 +29,13 @@ object AppContext { const val PAGE_SIZE = 50 const val TRANSITION_DURATION = 300L + val json = Json(JsonConfiguration(ignoreUnknownKeys = true)) + inline fun deserializer(serializer: DeserializationStrategy): ResponseDeserializable = - kotlinxDeserializerOf(loader = serializer, json = Json(JsonConfiguration(ignoreUnknownKeys = true))) + kotlinxDeserializerOf(loader = serializer, json = json) inline fun deserializer() = - kotlinxDeserializerOf(T::class.serializer(), Json(JsonConfiguration(ignoreUnknownKeys = true))) + kotlinxDeserializerOf(T::class.serializer(), json) fun init(context: Activity) { setupNotificationChannels(context) 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 e9d95e9..e62b208 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 @@ -8,7 +8,6 @@ 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 -import com.google.gson.Gson import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator import kotlinx.coroutines.CoroutineScope @@ -77,4 +76,4 @@ fun Request.authorize(): Request { } } -fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java) +fun Download.getMetadata(): DownloadInfo? = AppContext.json.parse(DownloadInfo.serializer(), String(this.request.data)) 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 index a8f5e47..9f0a722 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/AlbumsViewModel.kt @@ -3,19 +3,19 @@ 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 +import com.github.apognu.otter.repositories.AlbumsRepository +import com.github.apognu.otter.repositories.TracksRepository -class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() { +class AlbumsViewModel(private val repository: AlbumsRepository, private val tracksRepository: TracksRepository, private val artistId: Int? = null) : ViewModel() { val albums: LiveData> by lazy { if (artistId == null) { - Transformations.map(Otter.get().database.albums().allDecorated()) { + Transformations.map(repository.all()) { it.map { album -> Album.fromDecoratedEntity(album) } } } else { - Transformations.map(Otter.get().database.albums().forArtistDecorated(artistId)) { + Transformations.map(repository.ofArtist(artistId)) { it.map { album -> Album.fromDecoratedEntity(album) } } } @@ -23,24 +23,13 @@ class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() { 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 }) + val tracks = tracksRepository.ofArtistBlocking(artistId) return tracks.map { - Track.fromDecoratedEntity(it).apply { - this.uploads = uploads.filter { it.track_id == id }.map { Upload.fromEntity(it) } - } + Track.fromDecoratedEntity(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 index 9cfe6a5..c60414e 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt @@ -1,31 +1,13 @@ 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 +import com.github.apognu.otter.repositories.ArtistsRepository -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 ArtistsViewModel(private val repository: ArtistsRepository) : ViewModel() { + val artists: LiveData> = repository.all().map { artists -> + artists.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/FavoritesViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt index 91e58b8..a347374 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/FavoritesViewModel.kt @@ -1,22 +1,17 @@ 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 androidx.lifecycle.* import com.github.apognu.otter.models.domain.Album import com.github.apognu.otter.models.domain.Track -import com.github.apognu.otter.models.domain.Upload +import com.github.apognu.otter.repositories.FavoritesRepository +import com.github.apognu.otter.repositories.TracksRepository +import kotlinx.coroutines.delay -class FavoritesViewModel : ViewModel() { - companion object { - private lateinit var instance: FavoritesViewModel - - fun get(): FavoritesViewModel { - instance = if (::instance.isInitialized) instance else FavoritesViewModel() - - return instance +class FavoritesViewModel(private val repository: FavoritesRepository, private val tracksRepository: TracksRepository) : ViewModel() { + private val _downloaded = liveData { + while (true) { + emit(tracksRepository.downloaded()) + delay(5000) } } @@ -24,49 +19,40 @@ class FavoritesViewModel : ViewModel() { Transformations.switchMap(_favorites) { tracks -> val ids = tracks.mapNotNull { it.album?.id } - Transformations.map(Otter.get().database.albums().findAllDecorated(ids)) { albums -> + Transformations.map(repository.find(ids)) { albums -> albums.map { album -> Album.fromDecoratedEntity(album) } } } } private val _favorites: LiveData> by lazy { - Transformations.switchMap(Otter.get().database.favorites().all()) { + Transformations.switchMap(repository.all()) { val ids = it.map { favorite -> favorite.track_id } - Transformations.map(Otter.get().database.tracks().findAllDecorated(ids)) { tracks -> + Transformations.map(tracksRepository.find(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) } + addSource(_favorites) { merge(_favorites, _albums, _downloaded) } + addSource(_albums) { merge(_favorites, _albums, _downloaded) } + addSource(_downloaded) { merge(_favorites, _albums, _downloaded) } } - private fun merge(_tracks: LiveData>, _albums: LiveData>, _uploads: LiveData>) { + private fun merge(_tracks: LiveData>, _albums: LiveData>, _downloaded: LiveData>) { val _tracks = _tracks.value val _albums = _albums.value - val _uploads = _uploads.value + val _downloaded = _downloaded.value - if (_tracks == null || _albums == null || _uploads == null) { + if (_tracks == null || _albums == null || _downloaded == null) { return } favorites.value = _tracks.map { track -> - track.uploads = _uploads.filter { upload -> upload.track_id == track.id } + track.cached = tracksRepository.isCached(track) + track.downloaded = _downloaded.contains(track.id) track } } 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 index af9e918..41cdcdc 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/PlayerStateViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/PlayerStateViewModel.kt @@ -1,20 +1,10 @@ package com.github.apognu.otter.viewmodels import androidx.lifecycle.* -import com.github.apognu.otter.Otter +import com.github.apognu.otter.models.dao.OtterDatabase 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 - } - } - +class PlayerStateViewModel(private val database: OtterDatabase) : ViewModel() { val isPlaying: MutableLiveData by lazy { MutableLiveData() } val isBuffering: MutableLiveData by lazy { MutableLiveData() } val position: MutableLiveData> by lazy { MutableLiveData>() } @@ -27,7 +17,7 @@ class PlayerStateViewModel private constructor() : ViewModel() { return@switchMap null } - Otter.get().database.tracks().getDecorated(it.id).map { Track.fromDecoratedEntity(it) } + 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 index 1f791a7..831bcbf 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/PlaylistsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/PlaylistsViewModel.kt @@ -1,20 +1,53 @@ 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 androidx.lifecycle.* import com.github.apognu.otter.models.dao.PlaylistEntity import com.github.apognu.otter.models.domain.Track +import com.github.apognu.otter.repositories.PlaylistTracksRepository +import com.github.apognu.otter.repositories.PlaylistsRepository +import com.github.apognu.otter.repositories.TracksRepository +import kotlinx.coroutines.delay -class PlaylistsViewModel : ViewModel() { - val playlists: LiveData> by lazy { Otter.get().database.playlists().all() } +class PlaylistsViewModel(private val repository: PlaylistsRepository) : ViewModel() { + val playlists: LiveData> by lazy { repository.all() } } -class PlaylistViewModel(playlistId: Int) : ViewModel() { - val tracks: LiveData> by lazy { - Transformations.map(Otter.get().database.playlists().tracksFor(playlistId)) { +class PlaylistViewModel(private val repository: PlaylistTracksRepository, private val tracksRepository: TracksRepository, playerViewModel: PlayerStateViewModel, playlistId: Int) : ViewModel() { + private val _downloaded = liveData { + while (true) { + emit(tracksRepository.downloaded()) + delay(5000) + } + } + + private val _current = playerViewModel.track + + private val _tracks: LiveData> by lazy { + Transformations.map(repository.tracks(playlistId)) { it.map { track -> Track.fromDecoratedEntity(track) } } } + + val tracks = MediatorLiveData>().apply { + addSource(_tracks) { merge(_tracks, _current, _downloaded) } + addSource(_current) { merge(_tracks, _current, _downloaded) } + addSource(_downloaded) { merge(_tracks, _current, _downloaded) } + } + + private fun merge(_tracks: LiveData>, _current: LiveData, _downloaded: LiveData>) { + val _tracks = _tracks.value + val _current = _current.value + val _downloaded = _downloaded.value + + if (_tracks == null || _downloaded == null) { + return + } + + tracks.value = _tracks.map { track -> + track.current = _current?.id == track.id + track.cached = tracksRepository.isCached(track) + track.downloaded = _downloaded.contains(track.id) + 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 index 9f5e6ef..9dddc2c 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/QueueViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/QueueViewModel.kt @@ -7,19 +7,7 @@ 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) - +class QueueViewModel(private val repository: QueueRepository, playerViewModel: PlayerStateViewModel) : ViewModel() { private val _cached = liveData { while (true) { emit(Otter.get().exoCache.keys) @@ -27,10 +15,10 @@ class QueueViewModel private constructor() : ViewModel() { } } - private val _current = PlayerStateViewModel.get().track + private val _current = playerViewModel.track private val _queue: LiveData> by lazy { - Transformations.map(queueRepository.all()) { tracks -> + Transformations.map(repository.all()) { tracks -> tracks.map { Track.fromDecoratedEntity(it) } } } 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 index ec11c1b..3a03759 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/RadiosViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/RadiosViewModel.kt @@ -4,17 +4,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import com.github.apognu.otter.Otter import com.github.apognu.otter.models.dao.RadioEntity +import com.github.apognu.otter.repositories.RadiosRepository -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() } +class RadiosViewModel(private val repository: RadiosRepository) : ViewModel() { + val radios: LiveData> by lazy { repository.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 index b0ca0b3..18cb040 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/TracksViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/TracksViewModel.kt @@ -1,65 +1,65 @@ 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 com.github.apognu.otter.repositories.TracksRepository import kotlinx.coroutines.delay -class TracksViewModel(private val albumId: Int) : ViewModel() { - private val _cached = liveData { +class TracksViewModel(private val repository: TracksRepository, playerViewModel: PlayerStateViewModel, private val albumId: Int) : ViewModel() { + private val _downloaded = liveData { while (true) { - emit(Otter.get().exoCache.keys) + emit(repository.downloaded()) delay(5000) } } - private val _current = PlayerStateViewModel.get().track + private val _current = playerViewModel.track private val _tracks: LiveData> by lazy { - Transformations.map(Otter.get().database.tracks().ofAlbumsDecorated(listOf(albumId))) { + Transformations.map(repository.ofAlbums(listOf(albumId))) { it.map { track -> Track.fromDecoratedEntity(track) } } } private val _favorites: LiveData> by lazy { - Transformations.map(Otter.get().database.tracks().favorites()) { + Transformations.map(repository.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) } + addSource(_tracks) { mergeTracks(_tracks, _current, _downloaded) } + addSource(_current) { mergeTracks(_tracks, _current, _downloaded) } + addSource(_downloaded) { mergeTracks(_tracks, _current, _downloaded) } } val favorites = MediatorLiveData>().apply { - addSource(_favorites) { mergeFavorites(_favorites, _current, _cached) } - addSource(_current) { mergeFavorites(_favorites, _current, _cached) } - addSource(_cached) { mergeFavorites(_favorites, _current, _cached) } + addSource(_favorites) { mergeFavorites(_favorites, _current, _downloaded) } + addSource(_current) { mergeFavorites(_favorites, _current, _downloaded) } + addSource(_downloaded) { mergeFavorites(_favorites, _current, _downloaded) } } - private fun mergeTracks(_tracks: LiveData>, _current: LiveData, _cached: LiveData>) { - tracks.value = merge(_tracks, _current, _cached) ?: return + private fun mergeTracks(_tracks: LiveData>, _current: LiveData, _downloaded: LiveData>) { + tracks.value = merge(_tracks, _current, _downloaded) ?: return } - private fun mergeFavorites(_tracks: LiveData>, _current: LiveData, _cached: LiveData>) { - favorites.value = merge(_tracks, _current, _cached) ?: return + private fun mergeFavorites(_tracks: LiveData>, _current: LiveData, _downloaded: LiveData>) { + favorites.value = merge(_tracks, _current, _downloaded) ?: return } - private fun merge(_tracks: LiveData>, _current: LiveData, _cached: LiveData>): List? { + private fun merge(_tracks: LiveData>, _current: LiveData, _downloaded: LiveData>): List? { val _tracks = _tracks.value val _current = _current.value - val _cached = _cached.value + val _downloaded = _downloaded.value - if (_tracks == null || _cached == null) { + if (_tracks == null || _downloaded == null) { return null } return _tracks.map { track -> track.current = _current?.id == track.id - track.cached = _cached.contains(maybeNormalizeUrl(track.bestUpload()?.listen_url)) + track.cached = repository.isCached(track) + track.downloaded = _downloaded.contains(track.id) track } }