Added dependency injection.

This commit is contained in:
Antoine POPINEAU 2020-07-17 16:23:49 +02:00
parent 567a7476f9
commit 945f227ace
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
48 changed files with 593 additions and 454 deletions

View File

@ -127,6 +127,11 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.core:core-ktx:1.5.0-alpha02") implementation("androidx.core:core-ktx:1.5.0-alpha02")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
implementation("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.fragment:fragment-ktx:1.2.5")
implementation("androidx.room:room-runtime:2.2.5") implementation("androidx.room:room-runtime:2.2.5")
implementation("androidx.room:room-ktx: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-android:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0") implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3") implementation("com.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("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.2.1") implementation("jp.wasabeef:picasso-transformations:2.2.1")

View File

@ -1,15 +1,22 @@
package com.github.apognu.otter package com.github.apognu.otter
import android.app.Application import android.app.Application
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room 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.models.dao.OtterDatabase
import com.github.apognu.otter.playback.MediaSession import com.github.apognu.otter.playback.MediaSession
import com.github.apognu.otter.playback.QueueManager import com.github.apognu.otter.playback.QueueManager.Companion.factory
import com.github.apognu.otter.repositories.*
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Command import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.Event 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.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DefaultDownloadIndex import com.google.android.exoplayer2.offline.DefaultDownloadIndex
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
@ -20,7 +27,15 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.preference.PowerPreference import com.preference.PowerPreference
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel 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.text.SimpleDateFormat
import java.util.* import java.util.*
@ -36,12 +51,6 @@ class Otter : Application() {
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10) val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
val commandBus: BroadcastChannel<Command> = BroadcastChannel(10) val commandBus: BroadcastChannel<Command> = BroadcastChannel(10)
val database: OtterDatabase by lazy {
Room
.databaseBuilder(this, OtterDatabase::class.java, "otter")
.build()
}
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) } private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
val exoCache: SimpleCache by lazy { val exoCache: SimpleCache by lazy {
@ -65,7 +74,7 @@ class Otter : Application() {
} }
val exoDownloadManager: DownloadManager by lazy { val exoDownloadManager: DownloadManager by lazy {
DownloaderConstructorHelper(exoDownloadCache, QueueManager.factory(this)).run { DownloaderConstructorHelper(exoDownloadCache, factory(this)).run {
DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this)) DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
} }
} }
@ -83,6 +92,62 @@ class Otter : Application() {
instance = this 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")) { when (PowerPreference.getDefaultFile().getString("night_mode")) {
"on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"off" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) "off" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)

View File

@ -14,11 +14,9 @@ import com.github.apognu.otter.fragments.LoginDialog
import com.github.apognu.otter.models.api.Credentials import com.github.apognu.otter.models.api.Credentials
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Userinfo import com.github.apognu.otter.utils.Userinfo
import com.github.apognu.otter.utils.log
import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.result.Result import com.github.kittinunf.result.Result
import com.google.gson.Gson
import com.preference.PowerPreference import com.preference.PowerPreference
import kotlinx.android.synthetic.main.activity_login.* import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -131,12 +129,12 @@ class LoginActivity : AppCompatActivity() {
is Result.Failure -> { is Result.Failure -> {
dialog.dismiss() 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 hostname_field.error = null
username_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] username_field.error = error.non_field_errors[0]
} else { } else {
hostname_field.error = result.error.localizedMessage hostname_field.error = result.error.localizedMessage

View File

@ -18,11 +18,10 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.apognu.otter.Otter
import androidx.lifecycle.observe import androidx.lifecycle.observe
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.* import com.github.apognu.otter.fragments.*
import com.github.apognu.otter.models.dao.RealmArtist
import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.playback.MediaControlsManager import com.github.apognu.otter.playback.MediaControlsManager
import com.github.apognu.otter.playback.PinService import com.github.apognu.otter.playback.PinService
@ -31,16 +30,12 @@ import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.PlayerStateViewModel 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.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResponse import com.github.kittinunf.fuel.coroutines.awaitStringResponse
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.offline.DownloadService
import com.google.gson.Gson
import com.preference.PowerPreference import com.preference.PowerPreference
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import io.realm.Realm
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.partial_now_playing.* import kotlinx.android.synthetic.main.partial_now_playing.*
@ -50,17 +45,20 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.stringify
import org.koin.android.ext.android.inject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val playerViewModel by inject<PlayerStateViewModel>()
private val favoritesRepository by inject<FavoritesRepository>()
private val favoritedRepository by inject<FavoritedRepository>()
private val browseFragment by inject<BrowseFragment>()
private var menu: Menu? = null
enum class ResultCode(val code: Int) { enum class ResultCode(val code: Int) {
LOGOUT(1001) 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -70,17 +68,17 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(appbar) setSupportActionBar(appbar)
when (intent.action) { when (intent.action) {
MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment()) MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(inject<QueueFragment>().value)
} }
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.container, BrowseFragment()) .replace(R.id.container, browseFragment)
.commit() .commit()
watchEventBus() watchEventBus()
PlayerStateViewModel.get().isPlaying.observe(this) { isPlaying -> playerViewModel.isPlaying.observe(this) { isPlaying ->
when (isPlaying) { when (isPlaying) {
true -> { true -> {
now_playing_toggle.icon = getDrawable(R.drawable.pause) 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) { when (isBuffering) {
true -> now_playing_buffering.visibility = View.VISIBLE true -> now_playing_buffering.visibility = View.VISIBLE
false -> now_playing_buffering.visibility = View.GONE false -> now_playing_buffering.visibility = View.GONE
} }
} }
PlayerStateViewModel.get().track.observe(this) { track -> playerViewModel.track.observe(this) { track ->
refreshCurrentTrack(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_progress.progress = percent
now_playing_details_progress.progress = percent now_playing_details_progress.progress = percent
@ -187,7 +185,7 @@ class MainActivity : AppCompatActivity() {
}) })
landscape_queue?.let { landscape_queue?.let {
supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, LandscapeQueueFragment()).commit() supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, inject<LandscapeQueueFragment>().value).commit()
} }
} }
@ -231,7 +229,7 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
launchFragment(BrowseFragment()) launchFragment(browseFragment)
} }
R.id.nav_queue -> launchDialog(QueueFragment()) R.id.nav_queue -> launchDialog(QueueFragment())
@ -573,7 +571,7 @@ class MainActivity : AppCompatActivity() {
.post(mustNormalizeUrl("/api/v1/history/listenings/")) .post(mustNormalizeUrl("/api/v1/history/listenings/"))
.authorize() .authorize()
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Gson().toJson(mapOf("track" to track.id))) .body(AppContext.json.stringify(mapOf("track" to track.id)))
.awaitStringResponse() .awaitStringResponse()
} catch (_: Exception) { } catch (_: Exception) {
} }

View File

@ -5,36 +5,39 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.SearchAdapter import com.github.apognu.otter.adapters.SearchAdapter
import com.github.apognu.otter.fragments.AlbumsFragment import com.github.apognu.otter.fragments.AlbumsFragment
import com.github.apognu.otter.fragments.ArtistsFragment import com.github.apognu.otter.fragments.ArtistsFragment
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao 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.Album
import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.models.domain.Track 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 com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_search.* import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
class SearchActivity : AppCompatActivity() { class SearchActivity(private val database: OtterDatabase, private val favoritesRepository: FavoritesRepository) : AppCompatActivity() {
private lateinit var adapter: SearchAdapter private lateinit var adapter: SearchAdapter
lateinit var artistsRepository: ArtistsSearchRepository lateinit var artistsRepository: ArtistsSearchRepository
lateinit var albumsRepository: AlbumsSearchRepository lateinit var albumsRepository: AlbumsSearchRepository
lateinit var tracksRepository: TracksSearchRepository lateinit var tracksRepository: TracksSearchRepository
lateinit var favoritesRepository: FavoritesRepository
var done = 0 var done = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -64,7 +67,6 @@ class SearchActivity : AppCompatActivity() {
artistsRepository = ArtistsSearchRepository(this@SearchActivity, "") artistsRepository = ArtistsSearchRepository(this@SearchActivity, "")
albumsRepository = AlbumsSearchRepository(this@SearchActivity, "") albumsRepository = AlbumsSearchRepository(this@SearchActivity, "")
tracksRepository = TracksSearchRepository(this@SearchActivity, "") tracksRepository = TracksSearchRepository(this@SearchActivity, "")
favoritesRepository = FavoritesRepository(this@SearchActivity)
search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(rawQuery: String?): Boolean { override fun onQueryTextSubmit(rawQuery: String?): Boolean {
@ -92,7 +94,7 @@ class SearchActivity : AppCompatActivity() {
done++ done++
artists.forEach { artists.forEach {
Otter.get().database.artists().run { database.artists().run {
insert(it.toDao()) insert(it.toDao())
adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id))) adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id)))
@ -108,7 +110,7 @@ class SearchActivity : AppCompatActivity() {
done++ done++
albums.forEach { albums.forEach {
Otter.get().database.albums().run { database.albums().run {
insert(it.toDao()) insert(it.toDao())
adapter.albums.add(Album.fromDecoratedEntity(getDecoratedBlocking(it.id))) adapter.albums.add(Album.fromDecoratedEntity(getDecoratedBlocking(it.id)))
@ -124,8 +126,8 @@ class SearchActivity : AppCompatActivity() {
done++ done++
tracks.forEach { tracks.forEach {
Otter.get().database.tracks().run { database.tracks().run {
insertWithAssocs(it) insertWithAssocs(database.artists(), database.albums(), database.uploads(), it)
adapter.tracks.add(Track.fromDecoratedEntity(getDecoratedBlocking(it.id))) adapter.tracks.add(Track.fromDecoratedEntity(getDecoratedBlocking(it.id)))
} }

View File

@ -41,10 +41,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
.into(holder.art) .into(holder.art)
holder.name.text = artist.name holder.name.text = artist.name
holder.albums.text = context?.resources?.getQuantityString(R.plurals.album_count, artist.album_count, artist.album_count) ?: ""
context?.let {
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 { inner class ViewHolder(view: View, private val listener: OnArtistClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {

View File

@ -3,17 +3,17 @@ package com.github.apognu.otter.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.view.* import android.view.*
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.OtterAdapter import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.utils.*
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.* 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 private lateinit var touchHelper: ItemTouchHelper
var currentTrack: Track? = null
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
@ -68,15 +66,11 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
context?.let { context?.let {
holder.itemView.background = context.getDrawable(R.drawable.ripple) holder.itemView.background = context.getDrawable(R.drawable.ripple)
}
if (track == currentTrack) { if (track.current) {
context?.let {
holder.itemView.background = context.getDrawable(R.drawable.current) holder.itemView.background = context.getDrawable(R.drawable.current)
} }
}
context?.let {
when (track.favorite) { when (track.favorite) {
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite)) true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected)) false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
@ -85,6 +79,23 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
holder.favorite.setOnClickListener { holder.favorite.setOnClickListener {
favoriteListener?.onToggleFavorite(track.id, !track.favorite) 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 { holder.actions.setOnClickListener {
@ -96,7 +107,7 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
when (it.itemId) { when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track))) R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track)) R.id.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 true

View File

@ -6,8 +6,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.fragments.OtterAdapter import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Event import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.utils.EventBus import com.github.apognu.otter.utils.EventBus

View File

@ -57,6 +57,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
.maybeLoad(maybeNormalizeUrl(track.album?.cover())) .maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.fit() .fit()
.transform(RoundedCornersTransformation(8, 0)) .transform(RoundedCornersTransformation(8, 0))
.placeholder(R.drawable.cover)
.into(holder.cover) .into(holder.cover)
holder.title.text = track.title holder.title.text = track.title

View File

@ -8,7 +8,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
@ -18,27 +17,34 @@ import com.github.apognu.otter.R
import com.github.apognu.otter.activities.MainActivity import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.adapters.AlbumsAdapter import com.github.apognu.otter.adapters.AlbumsAdapter
import com.github.apognu.otter.models.api.FunkwhaleAlbum import com.github.apognu.otter.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.AlbumsRepository
import com.github.apognu.otter.repositories.ArtistTracksRepository import com.github.apognu.otter.repositories.ArtistTracksRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.viewmodels.AlbumsViewModel import com.github.apognu.otter.viewmodels.AlbumsViewModel
import com.github.apognu.otter.models.domain.Artist
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_albums.* import kotlinx.android.synthetic.main.fragment_albums.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch 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<FunkwhaleAlbum, Album, AlbumsAdapter>() { class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
override lateinit var liveData: LiveData<List<Album>> override val repository by inject<AlbumsRepository> { parametersOf(null) }
override val adapter by inject<AlbumsAdapter> { parametersOf(context, OnAlbumClickListener()) }
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(artistId) }
override val liveData by lazy { viewModel.albums }
override val viewRes = R.layout.fragment_albums override val viewRes = R.layout.fragment_albums
override val recycler: RecyclerView get() = albums override val recycler: RecyclerView get() = albums
override val alwaysRefresh = false override val alwaysRefresh = false
private lateinit var artistTracksRepository: ArtistTracksRepository private val artistTracksRepository by inject<ArtistTracksRepository> { parametersOf(artistId) }
var artistId = 0 private var artistId = 0
var artistName = "" var artistName = ""
var artistArt = "" var artistArt = ""
@ -93,19 +99,13 @@ class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply { arguments?.apply {
artistId = getInt("artistId") artistId = getInt("artistId")
artistName = getString("artistName") ?: "" artistName = getString("artistName") ?: ""
artistArt = getString("artistArt") ?: "" artistArt = getString("artistArt") ?: ""
} }
liveData = AlbumsViewModel(artistId).albums
super.onCreate(savedInstanceState)
adapter = AlbumsAdapter(context, OnAlbumClickListener())
repository = AlbumsRepository(context, artistId)
artistTracksRepository = ArtistTracksRepository(context, artistId)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -142,7 +142,7 @@ class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>()
play.isClickable = true play.isClickable = true
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
AlbumsViewModel(artistId).tracks().also { viewModel.tracks().also {
CommandBus.send(Command.ReplaceQueue(it.shuffled())) CommandBus.send(Command.ReplaceQueue(it.shuffled()))
} }
} }

View File

@ -1,6 +1,5 @@
package com.github.apognu.otter.fragments package com.github.apognu.otter.fragments
import android.os.Bundle
import android.view.View import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager 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.activities.MainActivity
import com.github.apognu.otter.adapters.AlbumsGridAdapter import com.github.apognu.otter.adapters.AlbumsGridAdapter
import com.github.apognu.otter.models.api.FunkwhaleAlbum import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.repositories.AlbumsRepository import com.github.apognu.otter.repositories.AlbumsRepository
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.viewmodels.AlbumsViewModel import com.github.apognu.otter.viewmodels.AlbumsViewModel
import kotlinx.android.synthetic.main.fragment_albums_grid.* 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<FunkwhaleAlbum, Album, AlbumsGridAdapter>() { class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
override val liveData = AlbumsViewModel().albums override val repository by inject<AlbumsRepository> { parametersOf(null) }
override val adapter by inject<AlbumsGridAdapter> { parametersOf(context, OnAlbumClickListener()) }
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(null) }
override val liveData by lazy { viewModel.albums }
override val viewRes = R.layout.fragment_albums_grid override val viewRes = R.layout.fragment_albums_grid
override val recycler: RecyclerView get() = albums override val recycler: RecyclerView get() = albums
override val layoutManager get() = GridLayoutManager(context, 3) override val layoutManager get() = GridLayoutManager(context, 3)
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = AlbumsGridAdapter(context, OnAlbumClickListener())
repository = AlbumsRepository(context)
}
inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener { inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener {
override fun onClick(view: View?, album: Album) { override fun onClick(view: View?, album: Album) {
(context as? MainActivity)?.let { activity -> (context as? MainActivity)?.let { activity ->

View File

@ -18,15 +18,22 @@ import com.github.apognu.otter.R
import com.github.apognu.otter.activities.MainActivity import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.adapters.ArtistsAdapter import com.github.apognu.otter.adapters.ArtistsAdapter
import com.github.apognu.otter.models.api.FunkwhaleArtist import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.onViewPager import com.github.apognu.otter.utils.onViewPager
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.viewmodels.ArtistsViewModel import com.github.apognu.otter.viewmodels.ArtistsViewModel
import kotlinx.android.synthetic.main.fragment_artists.* 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<FunkwhaleArtist, Artist, ArtistsAdapter>() { class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
override val liveData = ArtistsViewModel.get().artists override val repository by inject<ArtistsRepository>()
override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) }
override val viewModel by viewModel<ArtistsViewModel>()
override val liveData by lazy { viewModel.artists }
override val viewRes = R.layout.fragment_artists override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists override val recycler: RecyclerView get() = artists
@ -66,13 +73,6 @@ class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapte
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ArtistsAdapter(context, OnArtistClickListener())
repository = ArtistsRepository(context)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_artists, container, false) return inflater.inflate(R.layout.fragment_artists, container, false)
} }

View File

@ -13,26 +13,32 @@ import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.Command import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.CommandBus import com.github.apognu.otter.utils.CommandBus
import com.github.apognu.otter.utils.EventBus import com.github.apognu.otter.utils.EventBus
import com.github.apognu.otter.viewmodels.FavoritesViewModel
import com.github.apognu.otter.viewmodels.PlayerStateViewModel import com.github.apognu.otter.viewmodels.PlayerStateViewModel
import com.github.apognu.otter.viewmodels.TracksViewModel
import kotlinx.android.synthetic.main.fragment_favorites.* import kotlinx.android.synthetic.main.fragment_favorites.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class FavoritesFragment : LiveOtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() { class FavoritesFragment : LiveOtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
override val liveData = TracksViewModel(0).favorites override val repository by inject<FavoritesRepository>()
override val adapter by inject<FavoritesAdapter> { parametersOf(context, FavoriteListener()) }
override val viewModel by viewModel<FavoritesViewModel>()
override val liveData by lazy { viewModel.favorites }
override val viewRes = R.layout.fragment_favorites override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites override val recycler: RecyclerView get() = favorites
override val alwaysRefresh = false override val alwaysRefresh = false
private val playerViewModel by inject<PlayerStateViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = FavoritesAdapter(context, FavoriteListener()) playerViewModel.track.observe(this) { refreshCurrentTrack(it) }
repository = FavoritesRepository(context)
PlayerStateViewModel.get().track.observe(this) { refreshCurrentTrack(it) }
watchEventBus() watchEventBus()
} }

View File

@ -14,19 +14,15 @@ import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.viewmodels.QueueViewModel import com.github.apognu.otter.viewmodels.QueueViewModel
import kotlinx.android.synthetic.main.partial_queue.* import kotlinx.android.synthetic.main.partial_queue.*
import kotlinx.android.synthetic.main.partial_queue.view.* import kotlinx.android.synthetic.main.partial_queue.view.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class LandscapeQueueFragment : Fragment() { class LandscapeQueueFragment : Fragment() {
private val viewModel by viewModel<QueueViewModel>()
private val favoritesRepository by inject<FavoritesRepository>()
private var adapter: TracksAdapter? = null 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel.queue.observe(viewLifecycleOwner) { viewModel.queue.observe(viewLifecycleOwner) {
refresh(it) refresh(it)

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -34,20 +35,22 @@ abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adap
abstract override fun getItemId(position: Int): Long abstract override fun getItemId(position: Int): Long
} }
abstract class LiveOtterFragment<D : Any, DAO : Any, A : OtterAdapter<DAO, *>> : Fragment() { abstract class LiveOtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragment() {
companion object { companion object {
const val OFFSCREEN_PAGES = 20 const val OFFSCREEN_PAGES = 20
} }
abstract val liveData: LiveData<List<DAO>> abstract val repository: Repository<DAO>
abstract val adapter: A
open val viewModel: ViewModel? = null
abstract val liveData: LiveData<List<D>>
abstract val viewRes: Int abstract val viewRes: Int
abstract val recycler: RecyclerView abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context) open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true open val alwaysRefresh = true
lateinit var repository: Repository<D>
lateinit var adapter: A
private var moreLoading = false private var moreLoading = false
private var listener: Job? = null private var listener: Job? = null
@ -58,6 +61,13 @@ abstract class LiveOtterFragment<D : Any, DAO : Any, A : OtterAdapter<DAO, *>> :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
liveData.observe(viewLifecycleOwner) {
onDataUpdated(it)
adapter.data = it.toMutableList()
adapter.notifyDataSetChanged()
}
recycler.layoutManager = layoutManager recycler.layoutManager = layoutManager
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false (recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
recycler.adapter = adapter recycler.adapter = adapter
@ -88,17 +98,6 @@ abstract class LiveOtterFragment<D : Any, DAO : Any, A : OtterAdapter<DAO, *>> :
} }
} }
} }
fetch()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
liveData.observe(this) {
adapter.data = it.toMutableList()
adapter.notifyDataSetChanged()
}
} }
override fun onResume() { override fun onResume() {
@ -109,7 +108,8 @@ abstract class LiveOtterFragment<D : Any, DAO : Any, A : OtterAdapter<DAO, *>> :
} }
} }
open fun onDataFetched(data: List<D>) {} open fun onDataFetched(data: List<DAO>) {}
open fun onDataUpdated(data: List<D>?) {}
private fun fetch(size: Int = 0) { private fun fetch(size: Int = 0) {
moreLoading = true moreLoading = true

View File

@ -5,38 +5,41 @@ import android.view.Gravity
import android.view.View import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.PlaylistTracksAdapter import com.github.apognu.otter.adapters.PlaylistTracksAdapter
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack
import com.github.apognu.otter.models.dao.PlaylistEntity 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.FavoritesRepository
import com.github.apognu.otter.repositories.PlaylistTracksRepository import com.github.apognu.otter.repositories.PlaylistTracksRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.PlayerStateViewModel
import com.github.apognu.otter.viewmodels.PlaylistViewModel import com.github.apognu.otter.viewmodels.PlaylistViewModel
import com.github.apognu.otter.models.domain.Track
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_tracks.* import kotlinx.android.synthetic.main.fragment_tracks.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch 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<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() { class PlaylistTracksFragment : LiveOtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
override lateinit var liveData: LiveData<List<Track>> private val favoritesRepository by inject<FavoritesRepository>()
override val repository by inject<PlaylistTracksRepository> { parametersOf(playlistId) }
override val adapter by inject<PlaylistTracksAdapter> { parametersOf(context, FavoriteListener()) }
override val viewModel by viewModel<PlaylistViewModel> { parametersOf(playlistId) }
override val liveData by lazy { viewModel.tracks }
override val viewRes = R.layout.fragment_tracks override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks override val recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository
var playlistId = 0 var playlistId = 0
var playlistName = "" var playlistName = ""
companion object { companion object {
fun new(playlist: PlaylistEntity): PlaylistTracksFragment { fun new(playlist: PlaylistEntity, favoritesRepository: FavoritesRepository): PlaylistTracksFragment {
return PlaylistTracksFragment().apply { return PlaylistTracksFragment().apply {
arguments = bundleOf( arguments = bundleOf(
"playlistId" to playlist.id, "playlistId" to playlist.id,
@ -47,23 +50,12 @@ class PlaylistTracksFragment : LiveOtterFragment<FunkwhalePlaylistTrack, Track,
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply { arguments?.apply {
playlistId = getInt("playlistId") playlistId = getInt("playlistId")
playlistName = getString("playlistName") ?: "N/A" playlistName = getString("playlistName") ?: "N/A"
} }
liveData = PlaylistViewModel(playlistId).tracks
super.onCreate(savedInstanceState)
adapter = PlaylistTracksAdapter(context, FavoriteListener())
repository = PlaylistTracksRepository(context, playlistId)
favoritesRepository = FavoritesRepository(context)
PlayerStateViewModel.get().track.observe(this) { track ->
adapter.currentTrack = track
adapter.notifyDataSetChanged()
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -1,6 +1,5 @@
package com.github.apognu.otter.fragments package com.github.apognu.otter.fragments
import android.os.Bundle
import android.view.View import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.RecyclerView 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.adapters.PlaylistsAdapter
import com.github.apognu.otter.models.api.FunkwhalePlaylist import com.github.apognu.otter.models.api.FunkwhalePlaylist
import com.github.apognu.otter.models.dao.PlaylistEntity 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.repositories.PlaylistsRepository
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.viewmodels.PlaylistsViewModel import com.github.apognu.otter.viewmodels.PlaylistsViewModel
import kotlinx.android.synthetic.main.fragment_playlists.* 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<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() { class PlaylistsFragment : LiveOtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
override val liveData = PlaylistsViewModel().playlists override val repository by inject<PlaylistsRepository>()
override val adapter by inject<PlaylistsAdapter> { parametersOf(context, OnPlaylistClickListener()) }
override val viewModel by viewModel<PlaylistsViewModel>()
override val liveData by lazy { viewModel.playlists }
override val viewRes = R.layout.fragment_playlists override val viewRes = R.layout.fragment_playlists
override val recycler: RecyclerView get() = playlists override val recycler: RecyclerView get() = playlists
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) { private val favoritesRepository by inject<FavoritesRepository>()
super.onCreate(savedInstanceState)
adapter = PlaylistsAdapter(context, OnPlaylistClickListener())
repository = PlaylistsRepository(context)
}
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener { inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
override fun onClick(holder: View?, playlist: PlaylistEntity) { override fun onClick(holder: View?, playlist: PlaylistEntity) {
@ -41,7 +43,7 @@ class PlaylistsFragment : LiveOtterFragment<FunkwhalePlaylist, PlaylistEntity, P
} }
} }
val fragment = PlaylistTracksFragment.new(playlist).apply { val fragment = PlaylistTracksFragment.new(playlist, favoritesRepository).apply {
enterTransition = Slide().apply { enterTransition = Slide().apply {
duration = AppContext.TRANSITION_DURATION duration = AppContext.TRANSITION_DURATION
interpolator = AccelerateDecelerateInterpolator() interpolator = AccelerateDecelerateInterpolator()

View File

@ -21,18 +21,18 @@ import kotlinx.android.synthetic.main.fragment_queue.*
import kotlinx.android.synthetic.main.fragment_queue.view.* import kotlinx.android.synthetic.main.fragment_queue.view.*
import kotlinx.android.synthetic.main.partial_queue.* import kotlinx.android.synthetic.main.partial_queue.*
import kotlinx.android.synthetic.main.partial_queue.view.* import kotlinx.android.synthetic.main.partial_queue.view.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class QueueFragment : BottomSheetDialogFragment() { class QueueFragment : BottomSheetDialogFragment() {
private var adapter: TracksAdapter? = null private val viewModel by viewModel<QueueViewModel>()
private val favoritesRepository by inject<FavoritesRepository>()
private val viewModel = QueueViewModel.get() private var adapter: TracksAdapter? = null
lateinit var favoritesRepository: FavoritesRepository
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
favoritesRepository = FavoritesRepository(context)
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet) setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet)
} }

View File

@ -1,6 +1,5 @@
package com.github.apognu.otter.fragments package com.github.apognu.otter.fragments
import android.os.Bundle
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -15,20 +14,19 @@ import kotlinx.android.synthetic.main.fragment_radios.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() { class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
override val liveData = RadiosViewModel().radios override val repository by inject<RadiosRepository>()
override val adapter by inject<RadiosAdapter> { parametersOf(context, lifecycleScope, RadioClickListener()) }
override val viewModel by inject<RadiosViewModel>()
override val liveData by lazy { viewModel.radios }
override val viewRes = R.layout.fragment_radios override val viewRes = R.layout.fragment_radios
override val recycler: RecyclerView get() = radios override val recycler: RecyclerView get() = radios
override val alwaysRefresh = false override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = RadiosAdapter(context, lifecycleScope, RadioClickListener())
repository = RadiosRepository(context)
}
inner class RadioClickListener : RadiosAdapter.OnRadioClickListener { inner class RadioClickListener : RadiosAdapter.OnRadioClickListener {
override fun onClick(holder: RadiosAdapter.ViewHolder, radio: RadioEntity) { override fun onClick(holder: RadiosAdapter.ViewHolder, radio: RadioEntity) {
holder.spin() holder.spin()
@ -37,7 +35,6 @@ class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdap
it.isClickable = false it.isClickable = false
} }
// TOBEREDONE
CommandBus.send(Command.PlayRadio(radio)) CommandBus.send(Command.PlayRadio(radio))
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {

View File

@ -6,9 +6,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.TracksAdapter import com.github.apognu.otter.adapters.TracksAdapter
@ -19,7 +17,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.* import com.github.apognu.otter.viewmodels.TracksViewModel
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference import com.preference.PowerPreference
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
@ -30,19 +28,22 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>() { class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>() {
override lateinit var liveData: LiveData<List<Track>> override val repository by inject<TracksRepository> { parametersOf(albumId) }
override val adapter by inject<TracksAdapter> { parametersOf(context, FavoriteListener()) }
override val viewModel by viewModel<TracksViewModel> { parametersOf(albumId) }
override val liveData by lazy { viewModel.tracks }
override val viewRes = R.layout.fragment_tracks override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks override val recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository private val favoritesRepository by inject<FavoritesRepository>()
lateinit var favoritedRepository: FavoritedRepository
private var albumId = 0 private var albumId = 0
private var albumArtist = ""
private var albumTitle = ""
private var albumCover = ""
companion object { companion object {
fun new(album: Album): TracksFragment { fun new(album: Album): TracksFragment {
@ -53,35 +54,12 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply { arguments?.apply {
albumId = getInt("albumId") 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() watchEventBus()
} }
@ -156,6 +134,21 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
} }
} }
override fun onDataUpdated(data: List<Track>?) {
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() { private fun watchEventBus() {
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
EventBus.get().collect { message -> EventBus.get().collect { message ->

View File

@ -21,7 +21,7 @@ class OtterResponseSerializer<T : Any>(private val dataSerializer: KSerializer<T
} }
@Serializable @Serializable
data class Credentials(val token: String, val non_field_errors: List<String>? = null) data class Credentials(val token: String? = null, val non_field_errors: List<String>? = null)
@Serializable @Serializable
data class User(val full_username: String) data class User(val full_username: String)

View File

@ -2,6 +2,7 @@ package com.github.apognu.otter.models.api
import com.github.apognu.otter.models.domain.SearchResult import com.github.apognu.otter.models.domain.SearchResult
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import kotlinx.serialization.ContextualSerialization
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -55,10 +56,12 @@ data class FunkwhaleTrack(
@Serializable @Serializable
data class Favorited(val track: Int) data class Favorited(val track: Int)
@Serializable
data class DownloadInfo( data class DownloadInfo(
val id: Int, val id: Int,
val contentId: String, val contentId: String,
val title: String, val title: String,
val artist: String, val artist: String,
@ContextualSerialization
var download: Download? var download: Download?
) )

View File

@ -5,6 +5,7 @@ import androidx.room.*
import androidx.room.ForeignKey.CASCADE import androidx.room.ForeignKey.CASCADE
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import org.koin.java.KoinJavaComponent.inject
@Entity(tableName = "tracks") @Entity(tableName = "tracks")
data class TrackEntity( data class TrackEntity(
@ -59,17 +60,17 @@ data class TrackEntity(
fun insert(track: TrackEntity) fun insert(track: TrackEntity)
@Transaction @Transaction
fun insertWithAssocs(track: FunkwhaleTrack) { fun insertWithAssocs(artistsDao: ArtistEntity.Dao, albumsDao: AlbumEntity.Dao, uploadsDao: UploadEntity.Dao, track: FunkwhaleTrack) {
Otter.get().database.artists().insert(track.artist.toDao()) artistsDao.insert(track.artist.toDao())
track.album?.let { track.album?.let {
Otter.get().database.albums().insert(it.toDao()) albumsDao.insert(it.toDao())
} }
insert(track.toDao()) insert(track.toDao())
track.uploads.forEach { 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 ON ar.id = al.artist_id
LEFT JOIN favorites LEFT JOIN favorites
ON favorites.track_id = tracks.id ON favorites.track_id = tracks.id
ORDER BY tracks.position
""") """)
data class DecoratedTrackEntity( data class DecoratedTrackEntity(
val id: Int, val id: Int,

View File

@ -7,29 +7,27 @@ import android.net.Uri
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.models.api.DownloadInfo import com.github.apognu.otter.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.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.Download
import com.google.android.exoplayer2.offline.DownloadManager import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.offline.DownloadRequest import com.google.android.exoplayer2.offline.DownloadRequest
import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.offline.DownloadService
import com.google.android.exoplayer2.scheduler.Scheduler import com.google.android.exoplayer2.scheduler.Scheduler
import com.google.android.exoplayer2.ui.DownloadNotificationHelper import com.google.android.exoplayer2.ui.DownloadNotificationHelper
import com.google.gson.Gson import kotlinx.serialization.stringify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import java.util.* import java.util.*
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) { class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
companion object { companion object {
fun download(context: Context, track: Track) { fun download(context: Context, track: Track) {
track.bestUpload()?.let { upload -> track.bestUpload()?.let { upload ->
val url = mustNormalizeUrl(upload.listen_url) val url = mustNormalizeUrl(upload.listen_url)
val data = Gson().toJson( val data = AppContext.json.stringify(
DownloadInfo( DownloadInfo(
track.id, track.id,
url, url,

View File

@ -17,7 +17,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.PlayerStateViewModel import com.github.apognu.otter.viewmodels.PlayerStateViewModel
@ -32,8 +31,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.android.ext.android.inject
class PlayerService : Service() { class PlayerService : Service() {
private val playerViewModel by inject<PlayerStateViewModel>()
companion object { companion object {
const val INITIAL_COMMAND_KEY = "start_command" const val INITIAL_COMMAND_KEY = "start_command"
} }
@ -136,7 +138,7 @@ class PlayerService : Service() {
val (current, duration, percent) = getProgress(true) 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) { when (command) {
is Command.RefreshService -> { is Command.RefreshService -> {
if (queue.metadata.isNotEmpty()) { if (queue.metadata.isNotEmpty()) {
PlayerStateViewModel.get()._track.postValue(queue.current()) playerViewModel._track.postValue(queue.current())
PlayerStateViewModel.get().isPlaying.postValue(player.playWhenReady) playerViewModel.isPlaying.postValue(player.playWhenReady)
} }
} }
@ -211,7 +213,7 @@ class PlayerService : Service() {
delay(1000) delay(1000)
if (player.playWhenReady) { if (player.playWhenReady) {
PlayerStateViewModel.get().position.postValue(getProgress()) playerViewModel.position.postValue(getProgress())
} }
} }
} }
@ -271,7 +273,7 @@ class PlayerService : Service() {
if (hasAudioFocus(state)) { if (hasAudioFocus(state)) {
player.playWhenReady = state player.playWhenReady = state
PlayerStateViewModel.get().isPlaying.postValue(state) playerViewModel.isPlaying.postValue(state)
} }
} }
@ -291,7 +293,7 @@ class PlayerService : Service() {
player.next() player.next()
Cache.set(this@PlayerService, "progress", "0".toByteArray()) 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<Int, Int, Int> { private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
@ -371,17 +373,17 @@ class PlayerService : Service() {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState) super.onPlayerStateChanged(playWhenReady, playbackState)
PlayerStateViewModel.get().isPlaying.postValue(playWhenReady) playerViewModel.isPlaying.postValue(playWhenReady)
if (queue.current == -1) { if (queue.current == -1) {
PlayerStateViewModel.get()._track.postValue(queue.current()) playerViewModel._track.postValue(queue.current())
} }
when (playWhenReady) { when (playWhenReady) {
true -> { true -> {
when (playbackState) { when (playbackState) {
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true) Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
Player.STATE_BUFFERING -> PlayerStateViewModel.get().isBuffering.postValue(true) Player.STATE_BUFFERING -> playerViewModel.isBuffering.postValue(true)
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
setPlaybackState(false) 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 -> { false -> {
PlayerStateViewModel.get().isBuffering.postValue(false) playerViewModel.isBuffering.postValue(false)
Build.VERSION_CODES.N.onApi( Build.VERSION_CODES.N.onApi(
{ stopForeground(STOP_FOREGROUND_DETACH) }, { stopForeground(STOP_FOREGROUND_DETACH) },
@ -434,7 +436,7 @@ class PlayerService : Service() {
Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray()) 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) { override fun onPositionDiscontinuity(reason: Int) {

View File

@ -18,9 +18,13 @@ import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch 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) { class QueueManager(val context: Context) : KoinComponent {
private val queueRepository = QueueRepository(GlobalScope) private val playerViewModel by inject<PlayerStateViewModel>()
private val queueRepository by inject<QueueRepository> { parametersOf(GlobalScope) }
var metadata: MutableList<Track> = mutableListOf() var metadata: MutableList<Track> = mutableListOf()
val datasources = ConcatenatingMediaSource() val datasources = ConcatenatingMediaSource()
@ -59,7 +63,7 @@ class QueueManager(val context: Context) {
Cache.get(context, "current")?.let { string -> Cache.get(context, "current")?.let { string ->
current = string.readLine().toInt() current = string.readLine().toInt()
PlayerStateViewModel.get()._track.postValue(current()) playerViewModel._track.postValue(current())
} }
} }

View File

@ -1,17 +1,15 @@
package com.github.apognu.otter.playback package com.github.apognu.otter.playback
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.RadioEntity import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -19,6 +17,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.stringify
import org.koin.core.KoinComponent
import org.koin.core.inject
@Serializable @Serializable
data class RadioSessionBody(val radio_type: String?, var custom_radio: Int? = null, var related_object_id: String? = null) 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 @Serializable
data class RadioTrackID(val id: Int) data class RadioTrackID(val id: Int)
class RadioPlayer(val context: Context, val scope: CoroutineScope) { class RadioPlayer(val context: Context, val scope: CoroutineScope) : KoinComponent {
private val database by inject<OtterDatabase>()
val lock = Semaphore(1) val lock = Semaphore(1)
private var currentRadio: RadioEntity? = null private var currentRadio: RadioEntity? = null
private var session: Int? = null private var session: Int? = null
private var cookie: String? = null private var cookie: String? = null
private val favoritedRepository = FavoritedRepository(context)
init { init {
Cache.get(context, "radio_type")?.readLine()?.let { radio_type -> Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id -> 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 fun isActive() = currentRadio != null && session != null
private suspend fun createSession() { private suspend fun createSession() {
"createSession".log()
currentRadio?.let { radio -> currentRadio?.let { radio ->
try { try {
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply { val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
@ -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/")) val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
.authorize() .authorize()
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
@ -107,8 +106,6 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
prepareNextTrack(true) prepareNextTrack(true)
} catch (e: Exception) { } catch (e: Exception) {
e.log()
withContext(Main) { withContext(Main) {
context.toast(context.getString(R.string.radio_playback_error)) context.toast(context.getString(R.string.radio_playback_error))
} }
@ -117,11 +114,9 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
} }
suspend fun prepareNextTrack(first: Boolean = false) { suspend fun prepareNextTrack(first: Boolean = false) {
"prepareTrack".log()
session?.let { session -> session?.let { session ->
try { try {
val body = Gson().toJson(RadioTrackBody(session)) val body = AppContext.json.stringify(RadioTrackBody(session))
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/")) val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
.authorize() .authorize()
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
@ -138,8 +133,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
.awaitObjectResult<FunkwhaleTrack>(AppContext.deserializer()) .awaitObjectResult<FunkwhaleTrack>(AppContext.deserializer())
.get() .get()
Otter.get().database.tracks().run { database.tracks().run {
insertWithAssocs(track) insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
Track.fromDecoratedEntity(find(track.id)).let { track -> Track.fromDecoratedEntity(find(track.id)).let { track ->
if (first) { if (first) {

View File

@ -1,11 +1,16 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhaleAlbum 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 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<FunkwhaleAlbum>() { class AlbumsRepository(override val context: Context, private val database: OtterDatabase, artistId: Int?) : Repository<FunkwhaleAlbum>() {
override val upstream: Upstream<FunkwhaleAlbum> by lazy { override val upstream: Upstream<FunkwhaleAlbum> by lazy {
val url = val url =
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title" if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
@ -20,9 +25,19 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> { override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach { data.forEach {
Otter.get().database.albums().insert(it.toDao()) database.albums().insert(it.toDao())
} }
return super.onDataFetched(data) return super.onDataFetched(data)
} }
fun all() = database.albums().allDecorated()
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> {
scope.launch(Dispatchers.IO) {
fetch().collect()
}
return database.albums().forArtistDecorated(id)
}
} }

View File

@ -1,17 +1,17 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.OtterDatabase
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<FunkwhaleTrack>() { class ArtistTracksRepository(override val context: Context, private val database: OtterDatabase, artistId: Int) : Repository<FunkwhaleTrack>() {
override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", FunkwhaleTrack.serializer()) override val upstream = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", FunkwhaleTrack.serializer())
override fun onDataFetched(data: List<FunkwhaleTrack>) = runBlocking(IO) { override fun onDataFetched(data: List<FunkwhaleTrack>) = runBlocking(IO) {
data.forEach { data.forEach {
Otter.get().database.tracks().insertWithAssocs(it) database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it)
} }
super.onDataFetched(data) super.onDataFetched(data)

View File

@ -1,33 +1,45 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhaleArtist 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.toDao
import com.github.apognu.otter.models.dao.toRealmDao import com.github.apognu.otter.models.dao.toRealmDao
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ArtistsRepository(override val context: Context?) : Repository<FunkwhaleArtist>() { class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleArtist>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer()) HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> { override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
scope.launch(IO) { scope.launch(IO) {
data.forEach { artist -> data.forEach { artist ->
Otter.get().database.artists().insert(artist.toDao()) database.artists().insert(artist.toDao())
Realm.getDefaultInstance().executeTransaction { realm -> Realm.getDefaultInstance().executeTransaction { realm ->
realm.insertOrUpdate(artist.toRealmDao()) realm.insertOrUpdate(artist.toRealmDao())
} }
artist.albums?.forEach { album -> artist.albums?.forEach { album ->
Otter.get().database.albums().insert(album.toDao(artist.id)) database.albums().insert(album.toDao(artist.id))
} }
} }
} }
return super.onDataFetched(data) return super.onDataFetched(data)
} }
fun all(): LiveData<List<DecoratedArtistEntity>> {
scope.launch(IO) {
fetch().collect()
}
return database.artists().allDecorated()
}
fun get(id: Int) = database.artists().getDecorated(id)
} }

View File

@ -1,30 +1,32 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.Favorited import com.github.apognu.otter.models.api.Favorited
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.FavoriteEntity 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.Settings
import com.github.apognu.otter.utils.mustNormalizeUrl import com.github.apognu.otter.utils.mustNormalizeUrl
import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.stringify
class FavoritesRepository(override val context: Context?) : Repository<FunkwhaleTrack>() { class FavoritesRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleTrack>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", FunkwhaleTrack.serializer()) 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<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking { override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach { data.forEach {
Otter.get().database.tracks().insertWithAssocs(it) database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it)
Otter.get().database.favorites().insert(FavoriteEntity(it.id)) database.favorites().insert(FavoriteEntity(it.id))
} }
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf() /* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
@ -45,8 +47,18 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
data data
} }
fun all(): LiveData<List<FavoriteEntity>> {
scope.launch(IO) {
fetch().collect()
}
return database.favorites().all()
}
fun find(ids: List<Int>) = database.albums().findAllDecorated(ids)
fun addFavorite(id: Int) = scope.launch(IO) { fun addFavorite(id: Int) = scope.launch(IO) {
Otter.get().database.favorites().add(id) database.favorites().add(id)
val body = mapOf("track" to id) val body = mapOf("track" to id)
@ -59,7 +71,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
scope.launch(IO) { scope.launch(IO) {
request request
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Gson().toJson(body)) .body(AppContext.json.stringify(body))
.awaitByteArrayResponseResult() .awaitByteArrayResponseResult()
favoritedRepository.update() favoritedRepository.update()
@ -67,7 +79,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
} }
fun deleteFavorite(id: Int) = scope.launch(IO) { fun deleteFavorite(id: Int) = scope.launch(IO) {
Otter.get().database.favorites().remove(id) database.favorites().remove(id)
val body = mapOf("track" to id) val body = mapOf("track" to id)
@ -80,7 +92,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
scope.launch(IO) { scope.launch(IO) {
request request
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Gson().toJson(body)) .body(AppContext.json.stringify(body))
.awaitByteArrayResponseResult() .awaitByteArrayResponseResult()
favoritedRepository.update() favoritedRepository.update()
@ -88,14 +100,14 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
} }
} }
class FavoritedRepository(override val context: Context?) : Repository<Favorited>() { class FavoritedRepository(override val context: Context, private val database: OtterDatabase) : Repository<Favorited>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer()) HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer())
override fun onDataFetched(data: List<Favorited>): List<Favorited> { override fun onDataFetched(data: List<Favorited>): List<Favorited> {
scope.launch(IO) { scope.launch(IO) {
data.forEach { data.forEach {
Otter.get().database.favorites().insert(FavoriteEntity(it.track)) database.favorites().insert(FavoriteEntity(it.track))
} }
} }

View File

@ -52,9 +52,6 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
} }
}, },
{ error -> { error ->
"GET $url".log()
error.log()
when (error.exception) { when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut) is RefreshError -> EventBus.send(Event.LogOut)
else -> send(Repository.Response(listOf(), page, false)) else -> send(Repository.Response(listOf(), page, false))

View File

@ -1,24 +1,35 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack 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 com.github.apognu.otter.models.dao.PlaylistTrack
import kotlinx.coroutines.flow.map import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class PlaylistTracksRepository(override val context: Context?, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() { class PlaylistTracksRepository(override val context: Context?, private val database: OtterDatabase, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer()) HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer())
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>): List<FunkwhalePlaylistTrack> = runBlocking { override fun onDataFetched(data: List<FunkwhalePlaylistTrack>): List<FunkwhalePlaylistTrack> = runBlocking {
Otter.get().database.playlists().replaceTracks(playlistId, data.map { database.playlists().replaceTracks(playlistId, data.map {
Otter.get().database.tracks().insertWithAssocs(it.track) database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it.track)
PlaylistTrack(playlistId, it.track.id) PlaylistTrack(playlistId, it.track.id)
}) })
data data
} }
fun tracks(id: Int): LiveData<List<DecoratedTrackEntity>> {
scope.launch(Dispatchers.IO) {
fetch().collect()
}
return database.playlists().tracksFor(id)
}
} }

View File

@ -1,19 +1,41 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhalePlaylist 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 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<FunkwhalePlaylist>() { class PlaylistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhalePlaylist>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer()) HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer())
override fun onDataFetched(data: List<FunkwhalePlaylist>): List<FunkwhalePlaylist> { override fun onDataFetched(data: List<FunkwhalePlaylist>): List<FunkwhalePlaylist> {
data.forEach { data.forEach {
Otter.get().database.playlists().insert(it.toDao()) database.playlists().insert(it.toDao())
} }
return super.onDataFetched(data) return super.onDataFetched(data)
} }
fun all(): LiveData<List<PlaylistEntity>> {
scope.launch(IO) {
fetch().collect()
}
return database.playlists().all()
}
fun tracks(id: Int): LiveData<List<DecoratedTrackEntity>> {
scope.launch(IO) {
fetch().collect()
}
return database.playlists().tracksFor(id)
}
} }

View File

@ -1,16 +1,16 @@
package com.github.apognu.otter.repositories 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 com.github.apognu.otter.models.domain.Track
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class QueueRepository(val scope: CoroutineScope) { class QueueRepository(private val database: OtterDatabase, private val scope: CoroutineScope) {
fun all() = Otter.get().database.queue().allDecorated() fun all() = database.queue().allDecorated()
fun allBlocking() = Otter.get().database.queue().allDecoratedBlocking() fun allBlocking() = database.queue().allDecoratedBlocking()
fun replace(tracks: List<Track>) = scope.launch { fun replace(tracks: List<Track>) = scope.launch {
Otter.get().database.queue().replace(tracks) database.queue().replace(tracks)
} }
} }

View File

@ -1,21 +1,32 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhaleRadio 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 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<FunkwhaleRadio>() { class RadiosRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleRadio>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer()) HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer())
override fun onDataFetched(data: List<FunkwhaleRadio>): List<FunkwhaleRadio> { override fun onDataFetched(data: List<FunkwhaleRadio>): List<FunkwhaleRadio> {
data.forEach { data.forEach {
Otter.get().database.radios().insert(it.toDao()) database.radios().insert(it.apply { radio_type = "custom" }.toDao())
} }
return data return data
.map { radio -> radio.apply { radio_type = "custom" } } }
.toMutableList()
fun all(): LiveData<List<RadioEntity>> {
scope.launch(Dispatchers.IO) {
fetch().collect()
}
return database.radios().all()
} }
} }

View File

@ -4,8 +4,6 @@ import android.content.Context
import com.github.apognu.otter.models.api.FunkwhaleAlbum import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.api.FunkwhaleArtist import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleTrack>() { class TracksSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleTrack>() {
@ -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()) get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking { override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
val favorites = FavoritedRepository(context).fetch()
.map { it.data }
.toList()
.flatten()
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf() /* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
data.map { track -> data.map { track ->

View File

@ -1,13 +1,21 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack 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.getMetadata
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.google.android.exoplayer2.offline.Download 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 import kotlinx.coroutines.runBlocking
class TracksRepository(override val context: Context?, albumId: Int) : Repository<FunkwhaleTrack>() { class TracksRepository(override val context: Context, private val database: OtterDatabase, albumId: Int?) : Repository<FunkwhaleTrack>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer()) 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<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking { override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach { track -> 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 })) data.sortedWith(compareBy({ it.disc_number }, { it.position }))
} }
fun find(ids: List<Int>) = database.tracks().findAllDecorated(ids)
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)
fun ofAlbums(albums: List<Int>): LiveData<List<DecoratedTrackEntity>> {
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<Int> {
val cursor = Otter.get().exoDownloadManager.downloadIndex.getDownloads()
val ids: MutableList<Int> = mutableListOf()
while (cursor.moveToNext()) {
val download = cursor.download
download.getMetadata()?.let {
if (download.state == Download.STATE_COMPLETED) {
ids.add(it.id)
}
}
}
return ids
}
} }

View File

@ -29,11 +29,13 @@ object AppContext {
const val PAGE_SIZE = 50 const val PAGE_SIZE = 50
const val TRANSITION_DURATION = 300L const val TRANSITION_DURATION = 300L
val json = Json(JsonConfiguration(ignoreUnknownKeys = true))
inline fun <reified T : Any> deserializer(serializer: DeserializationStrategy<T>): ResponseDeserializable<T> = inline fun <reified T : Any> deserializer(serializer: DeserializationStrategy<T>): ResponseDeserializable<T> =
kotlinxDeserializerOf(loader = serializer, json = Json(JsonConfiguration(ignoreUnknownKeys = true))) kotlinxDeserializerOf(loader = serializer, json = json)
inline fun <reified T : Any> deserializer() = inline fun <reified T : Any> deserializer() =
kotlinxDeserializerOf(T::class.serializer(), Json(JsonConfiguration(ignoreUnknownKeys = true))) kotlinxDeserializerOf(T::class.serializer(), json)
fun init(context: Activity) { fun init(context: Activity) {
setupNotificationChannels(context) setupNotificationChannels(context)

View File

@ -8,7 +8,6 @@ import com.github.apognu.otter.models.api.DownloadInfo
import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.repositories.Repository
import com.github.kittinunf.fuel.core.Request import com.github.kittinunf.fuel.core.Request
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator import com.squareup.picasso.RequestCreator
import kotlinx.coroutines.CoroutineScope 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))

View File

@ -3,19 +3,19 @@ package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel 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.Album
import com.github.apognu.otter.models.domain.Track 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<List<Album>> by lazy { val albums: LiveData<List<Album>> by lazy {
if (artistId == null) { if (artistId == null) {
Transformations.map(Otter.get().database.albums().allDecorated()) { Transformations.map(repository.all()) {
it.map { album -> Album.fromDecoratedEntity(album) } it.map { album -> Album.fromDecoratedEntity(album) }
} }
} else { } else {
Transformations.map(Otter.get().database.albums().forArtistDecorated(artistId)) { Transformations.map(repository.ofArtist(artistId)) {
it.map { album -> Album.fromDecoratedEntity(album) } it.map { album -> Album.fromDecoratedEntity(album) }
} }
} }
@ -23,24 +23,13 @@ class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() {
suspend fun tracks(): List<Track> { suspend fun tracks(): List<Track> {
artistId?.let { artistId?.let {
val tracks = Otter.get().database.tracks().ofArtistBlocking(artistId) val tracks = tracksRepository.ofArtistBlocking(artistId)
val uploads = Otter.get().database.uploads().findAllBlocking(tracks.map { it.id })
return tracks.map { return tracks.map {
Track.fromDecoratedEntity(it).apply { Track.fromDecoratedEntity(it)
this.uploads = uploads.filter { it.track_id == id }.map { Upload.fromEntity(it) }
}
} }
} }
return listOf() return listOf()
} }
} }
class AlbumViewModel(private val id: Int) : ViewModel() {
val album: LiveData<Album> by lazy {
Transformations.map(Otter.get().database.albums().getDecorated(id)) { album ->
Album.fromDecoratedEntity(album)
}
}
}

View File

@ -1,31 +1,13 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations.map
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository
class ArtistsViewModel : ViewModel() { class ArtistsViewModel(private val repository: ArtistsRepository) : ViewModel() {
companion object { val artists: LiveData<List<Artist>> = repository.all().map { artists ->
private lateinit var instance: ArtistsViewModel artists.map { Artist.fromDecoratedEntity(it) }
fun get(): ArtistsViewModel {
instance = if (::instance.isInitialized) instance else ArtistsViewModel()
return instance
}
}
val artists: LiveData<List<Artist>> = Otter.get().database.artists().allDecorated().map {
it.map { Artist.fromDecoratedEntity(it) }
} }
} }
class ArtistViewModel(private val id: Int) : ViewModel() {
val artist: LiveData<Artist> by lazy {
map(Otter.get().database.artists().getDecorated(id)) { artist ->
Artist.fromDecoratedEntity(artist)
}
}
}

View File

@ -1,22 +1,17 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.*
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.domain.Album import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.models.domain.Track 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() { class FavoritesViewModel(private val repository: FavoritesRepository, private val tracksRepository: TracksRepository) : ViewModel() {
companion object { private val _downloaded = liveData {
private lateinit var instance: FavoritesViewModel while (true) {
emit(tracksRepository.downloaded())
fun get(): FavoritesViewModel { delay(5000)
instance = if (::instance.isInitialized) instance else FavoritesViewModel()
return instance
} }
} }
@ -24,49 +19,40 @@ class FavoritesViewModel : ViewModel() {
Transformations.switchMap(_favorites) { tracks -> Transformations.switchMap(_favorites) { tracks ->
val ids = tracks.mapNotNull { it.album?.id } 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) } albums.map { album -> Album.fromDecoratedEntity(album) }
} }
} }
} }
private val _favorites: LiveData<List<Track>> by lazy { private val _favorites: LiveData<List<Track>> by lazy {
Transformations.switchMap(Otter.get().database.favorites().all()) { Transformations.switchMap(repository.all()) {
val ids = it.map { favorite -> favorite.track_id } 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 } tracks.map { track -> Track.fromDecoratedEntity(track) }.sortedBy { it.title }
} }
} }
} }
private val _uploads: LiveData<List<Upload>> by lazy {
Transformations.switchMap(_favorites) { tracks ->
val ids = tracks.mapNotNull { it.album?.id }
Transformations.map(Otter.get().database.uploads().findAll(ids)) { uploads ->
uploads.map { upload -> Upload.fromEntity(upload) }
}
}
}
val favorites = MediatorLiveData<List<Track>>().apply { val favorites = MediatorLiveData<List<Track>>().apply {
addSource(_favorites) { merge(_favorites, _albums, _uploads) } addSource(_favorites) { merge(_favorites, _albums, _downloaded) }
addSource(_albums) { merge(_favorites, _albums, _uploads) } addSource(_albums) { merge(_favorites, _albums, _downloaded) }
addSource(_uploads) { merge(_favorites, _albums, _uploads) } addSource(_downloaded) { merge(_favorites, _albums, _downloaded) }
} }
private fun merge(_tracks: LiveData<List<Track>>, _albums: LiveData<List<Album>>, _uploads: LiveData<List<Upload>>) { private fun merge(_tracks: LiveData<List<Track>>, _albums: LiveData<List<Album>>, _downloaded: LiveData<List<Int>>) {
val _tracks = _tracks.value val _tracks = _tracks.value
val _albums = _albums.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 return
} }
favorites.value = _tracks.map { track -> 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 track
} }
} }

View File

@ -1,20 +1,10 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.* 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 import com.github.apognu.otter.models.domain.Track
class PlayerStateViewModel private constructor() : ViewModel() { class PlayerStateViewModel(private val database: OtterDatabase) : ViewModel() {
companion object {
private lateinit var instance: PlayerStateViewModel
fun get(): PlayerStateViewModel {
instance = if (::instance.isInitialized) instance else PlayerStateViewModel()
return instance
}
}
val isPlaying: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() } val isPlaying: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val isBuffering: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() } val isBuffering: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
val position: MutableLiveData<Triple<Int, Int, Int>> by lazy { MutableLiveData<Triple<Int, Int, Int>>() } val position: MutableLiveData<Triple<Int, Int, Int>> by lazy { MutableLiveData<Triple<Int, Int, Int>>() }
@ -27,7 +17,7 @@ class PlayerStateViewModel private constructor() : ViewModel() {
return@switchMap null return@switchMap null
} }
Otter.get().database.tracks().getDecorated(it.id).map { Track.fromDecoratedEntity(it) } database.tracks().getDecorated(it.id).map { Track.fromDecoratedEntity(it) }
} }
} }
} }

View File

@ -1,20 +1,53 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.*
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.dao.PlaylistEntity import com.github.apognu.otter.models.dao.PlaylistEntity
import com.github.apognu.otter.models.domain.Track 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() { class PlaylistsViewModel(private val repository: PlaylistsRepository) : ViewModel() {
val playlists: LiveData<List<PlaylistEntity>> by lazy { Otter.get().database.playlists().all() } val playlists: LiveData<List<PlaylistEntity>> by lazy { repository.all() }
} }
class PlaylistViewModel(playlistId: Int) : ViewModel() { class PlaylistViewModel(private val repository: PlaylistTracksRepository, private val tracksRepository: TracksRepository, playerViewModel: PlayerStateViewModel, playlistId: Int) : ViewModel() {
val tracks: LiveData<List<Track>> by lazy { private val _downloaded = liveData {
Transformations.map(Otter.get().database.playlists().tracksFor(playlistId)) { while (true) {
emit(tracksRepository.downloaded())
delay(5000)
}
}
private val _current = playerViewModel.track
private val _tracks: LiveData<List<Track>> by lazy {
Transformations.map(repository.tracks(playlistId)) {
it.map { track -> Track.fromDecoratedEntity(track) } it.map { track -> Track.fromDecoratedEntity(track) }
} }
} }
val tracks = MediatorLiveData<List<Track>>().apply {
addSource(_tracks) { merge(_tracks, _current, _downloaded) }
addSource(_current) { merge(_tracks, _current, _downloaded) }
addSource(_downloaded) { merge(_tracks, _current, _downloaded) }
}
private fun merge(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _downloaded: LiveData<List<Int>>) {
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
}
}
} }

View File

@ -7,19 +7,7 @@ import com.github.apognu.otter.repositories.QueueRepository
import com.github.apognu.otter.utils.maybeNormalizeUrl import com.github.apognu.otter.utils.maybeNormalizeUrl
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class QueueViewModel private constructor() : ViewModel() { class QueueViewModel(private val repository: QueueRepository, playerViewModel: PlayerStateViewModel) : ViewModel() {
companion object {
private lateinit var instance: QueueViewModel
fun get(): QueueViewModel {
instance = if (::instance.isInitialized) instance else QueueViewModel()
return instance
}
}
private val queueRepository = QueueRepository(viewModelScope)
private val _cached = liveData { private val _cached = liveData {
while (true) { while (true) {
emit(Otter.get().exoCache.keys) 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<List<Track>> by lazy { private val _queue: LiveData<List<Track>> by lazy {
Transformations.map(queueRepository.all()) { tracks -> Transformations.map(repository.all()) { tracks ->
tracks.map { Track.fromDecoratedEntity(it) } tracks.map { Track.fromDecoratedEntity(it) }
} }
} }

View File

@ -4,17 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.dao.RadioEntity import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.repositories.RadiosRepository
class RadiosViewModel : ViewModel() { class RadiosViewModel(private val repository: RadiosRepository) : ViewModel() {
companion object { val radios: LiveData<List<RadioEntity>> by lazy { repository.all() }
private lateinit var instance: RadiosViewModel
fun get(): RadiosViewModel {
instance = if (::instance.isInitialized) instance else RadiosViewModel()
return instance
}
}
val radios: LiveData<List<RadioEntity>> by lazy { Otter.get().database.radios().all() }
} }

View File

@ -1,65 +1,65 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.* import androidx.lifecycle.*
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.domain.Track 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 import kotlinx.coroutines.delay
class TracksViewModel(private val albumId: Int) : ViewModel() { class TracksViewModel(private val repository: TracksRepository, playerViewModel: PlayerStateViewModel, private val albumId: Int) : ViewModel() {
private val _cached = liveData { private val _downloaded = liveData {
while (true) { while (true) {
emit(Otter.get().exoCache.keys) emit(repository.downloaded())
delay(5000) delay(5000)
} }
} }
private val _current = PlayerStateViewModel.get().track private val _current = playerViewModel.track
private val _tracks: LiveData<List<Track>> by lazy { private val _tracks: LiveData<List<Track>> by lazy {
Transformations.map(Otter.get().database.tracks().ofAlbumsDecorated(listOf(albumId))) { Transformations.map(repository.ofAlbums(listOf(albumId))) {
it.map { track -> Track.fromDecoratedEntity(track) } it.map { track -> Track.fromDecoratedEntity(track) }
} }
} }
private val _favorites: LiveData<List<Track>> by lazy { private val _favorites: LiveData<List<Track>> by lazy {
Transformations.map(Otter.get().database.tracks().favorites()) { Transformations.map(repository.favorites()) {
it.map { track -> Track.fromDecoratedEntity(track) } it.map { track -> Track.fromDecoratedEntity(track) }
} }
} }
val tracks = MediatorLiveData<List<Track>>().apply { val tracks = MediatorLiveData<List<Track>>().apply {
addSource(_tracks) { mergeTracks(_tracks, _current, _cached) } addSource(_tracks) { mergeTracks(_tracks, _current, _downloaded) }
addSource(_current) { mergeTracks(_tracks, _current, _cached) } addSource(_current) { mergeTracks(_tracks, _current, _downloaded) }
addSource(_cached) { mergeTracks(_tracks, _current, _cached) } addSource(_downloaded) { mergeTracks(_tracks, _current, _downloaded) }
} }
val favorites = MediatorLiveData<List<Track>>().apply { val favorites = MediatorLiveData<List<Track>>().apply {
addSource(_favorites) { mergeFavorites(_favorites, _current, _cached) } addSource(_favorites) { mergeFavorites(_favorites, _current, _downloaded) }
addSource(_current) { mergeFavorites(_favorites, _current, _cached) } addSource(_current) { mergeFavorites(_favorites, _current, _downloaded) }
addSource(_cached) { mergeFavorites(_favorites, _current, _cached) } addSource(_downloaded) { mergeFavorites(_favorites, _current, _downloaded) }
} }
private fun mergeTracks(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>) { private fun mergeTracks(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _downloaded: LiveData<List<Int>>) {
tracks.value = merge(_tracks, _current, _cached) ?: return tracks.value = merge(_tracks, _current, _downloaded) ?: return
} }
private fun mergeFavorites(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>) { private fun mergeFavorites(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _downloaded: LiveData<List<Int>>) {
favorites.value = merge(_tracks, _current, _cached) ?: return favorites.value = merge(_tracks, _current, _downloaded) ?: return
} }
private fun merge(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _cached: LiveData<Set<String>>): List<Track>? { private fun merge(_tracks: LiveData<List<Track>>, _current: LiveData<Track>, _downloaded: LiveData<List<Int>>): List<Track>? {
val _tracks = _tracks.value val _tracks = _tracks.value
val _current = _current.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 null
} }
return _tracks.map { track -> return _tracks.map { track ->
track.current = _current?.id == track.id 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 track
} }
} }