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.core:core-ktx:1.5.0-alpha02")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
implementation("org.koin:koin-core:2.1.6")
implementation("org.koin:koin-android:2.1.6")
implementation("org.koin:koin-androidx-scope:2.1.6")
implementation("org.koin:koin-androidx-viewmodel:2.1.6")
implementation("org.koin:koin-androidx-fragment:2.1.6")
implementation("androidx.fragment:fragment-ktx:1.2.5")
implementation("androidx.room:room-runtime:2.2.5")
implementation("androidx.room:room-ktx:2.2.5")
@ -148,7 +153,6 @@ dependencies {
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.2.1")

View File

@ -1,15 +1,22 @@
package com.github.apognu.otter
import android.app.Application
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room
import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.activities.SearchActivity
import com.github.apognu.otter.adapters.*
import com.github.apognu.otter.fragments.*
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.playback.MediaSession
import com.github.apognu.otter.playback.QueueManager
import com.github.apognu.otter.playback.QueueManager.Companion.factory
import com.github.apognu.otter.repositories.*
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.viewmodels.*
import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
@ -20,7 +27,15 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.preference.PowerPreference
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.fragment.dsl.fragment
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module
import java.text.SimpleDateFormat
import java.util.*
@ -36,12 +51,6 @@ class Otter : Application() {
val eventBus: BroadcastChannel<Event> = 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) }
val exoCache: SimpleCache by lazy {
@ -65,7 +74,7 @@ class Otter : Application() {
}
val exoDownloadManager: DownloadManager by lazy {
DownloaderConstructorHelper(exoDownloadCache, QueueManager.factory(this)).run {
DownloaderConstructorHelper(exoDownloadCache, factory(this)).run {
DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
}
}
@ -83,6 +92,62 @@ class Otter : Application() {
instance = this
startKoin {
androidLogger()
androidContext(this@Otter)
modules(module {
single {
synchronized(this) {
Room
.databaseBuilder(get(), OtterDatabase::class.java, "otter")
.build()
}
}
factory { MainActivity() }
factory { SearchActivity(get(), get()) }
fragment { BrowseFragment() }
fragment { LandscapeQueueFragment() }
single { PlayerStateViewModel(get()) }
single { ArtistsRepository(get(), get()) }
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
viewModel { ArtistsViewModel(get()) }
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
viewModel { (id: Int?) -> AlbumsViewModel(get { parametersOf(id) }, get { parametersOf(id) }, id) }
factory { (context: Context?, adapter: AlbumsAdapter.OnAlbumClickListener) -> AlbumsAdapter(context, adapter) }
factory { (context: Context?, adapter: AlbumsGridAdapter.OnAlbumClickListener) -> AlbumsGridAdapter(context, adapter) }
factory { (id: Int?) -> TracksRepository(get(), get(), id) }
viewModel { (id: Int) -> TracksViewModel(get { parametersOf(id) }, get(), id) }
factory { (context: Context?, favoriteListener: TracksAdapter.OnFavoriteListener?) -> TracksAdapter(context, favoriteListener) }
single { PlaylistsRepository(get(), get()) }
factory { (id: Int) -> PlaylistTracksRepository(get(), get(), id) }
viewModel { PlaylistsViewModel(get()) }
viewModel { (id: Int) -> PlaylistViewModel(get { parametersOf(id) }, get { parametersOf(null) }, get(), id) }
factory { (context: Context?, listener: PlaylistsAdapter.OnPlaylistClickListener) -> PlaylistsAdapter(context, listener) }
factory { (context: Context?, listener: PlaylistTracksAdapter.OnFavoriteListener) -> PlaylistTracksAdapter(context, listener) }
single { FavoritesRepository(get(), get()) }
single { FavoritedRepository(get(), get()) }
factory { (context: Context?, listener: FavoritesAdapter.OnFavoriteListener) -> FavoritesAdapter(context, listener) }
viewModel { FavoritesViewModel(get(), get { parametersOf(null) }) }
single { RadiosRepository(get(), get()) }
factory { (context: Context?, scope: CoroutineScope, listener: RadiosAdapter.OnRadioClickListener) -> RadiosAdapter(context, scope, listener) }
viewModel { RadiosViewModel(get()) }
single { (scope: CoroutineScope) -> QueueRepository(get(), scope) }
viewModel { QueueViewModel(get(), get()) }
})
}
when (PowerPreference.getDefaultFile().getString("night_mode")) {
"on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"off" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)

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.utils.AppContext
import com.github.apognu.otter.utils.Userinfo
import com.github.apognu.otter.utils.log
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.result.Result
import com.google.gson.Gson
import com.preference.PowerPreference
import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.Dispatchers.Main
@ -131,12 +129,12 @@ class LoginActivity : AppCompatActivity() {
is Result.Failure -> {
dialog.dismiss()
val error = Gson().fromJson(String(response.data), Credentials::class.java)
val error = AppContext.json.parse(Credentials.serializer(), String(response.data))
hostname_field.error = null
username_field.error = null
if (error != null && error.non_field_errors?.isNotEmpty() == true) {
if (error.non_field_errors?.isNotEmpty() == true) {
username_field.error = error.non_field_errors[0]
} else {
hostname_field.error = result.error.localizedMessage

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,15 +18,22 @@ import com.github.apognu.otter.R
import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.adapters.ArtistsAdapter
import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.onViewPager
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.viewmodels.ArtistsViewModel
import kotlinx.android.synthetic.main.fragment_artists.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class ArtistsFragment : LiveOtterFragment<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 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? {
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.CommandBus
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.TracksViewModel
import kotlinx.android.synthetic.main.fragment_favorites.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
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>() {
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 recycler: RecyclerView get() = favorites
override val alwaysRefresh = false
private val playerViewModel by inject<PlayerStateViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = FavoritesAdapter(context, FavoriteListener())
repository = FavoritesRepository(context)
PlayerStateViewModel.get().track.observe(this) { refreshCurrentTrack(it) }
playerViewModel.track.observe(this) { refreshCurrentTrack(it) }
watchEventBus()
}

View File

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

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
@ -34,20 +35,22 @@ abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adap
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 {
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 recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true
lateinit var repository: Repository<D>
lateinit var adapter: A
private var moreLoading = false
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?) {
super.onViewCreated(view, savedInstanceState)
liveData.observe(viewLifecycleOwner) {
onDataUpdated(it)
adapter.data = it.toMutableList()
adapter.notifyDataSetChanged()
}
recycler.layoutManager = layoutManager
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
recycler.adapter = adapter
@ -88,17 +98,6 @@ abstract class LiveOtterFragment<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() {
@ -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) {
moreLoading = true

View File

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

View File

@ -1,6 +1,5 @@
package com.github.apognu.otter.fragments
import android.os.Bundle
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.RecyclerView
@ -11,23 +10,26 @@ import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.adapters.PlaylistsAdapter
import com.github.apognu.otter.models.api.FunkwhalePlaylist
import com.github.apognu.otter.models.dao.PlaylistEntity
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.PlaylistsRepository
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.viewmodels.PlaylistsViewModel
import kotlinx.android.synthetic.main.fragment_playlists.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class PlaylistsFragment : LiveOtterFragment<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 recycler: RecyclerView get() = playlists
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = PlaylistsAdapter(context, OnPlaylistClickListener())
repository = PlaylistsRepository(context)
}
private val favoritesRepository by inject<FavoritesRepository>()
inner class OnPlaylistClickListener : PlaylistsAdapter.OnPlaylistClickListener {
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 {
duration = AppContext.TRANSITION_DURATION
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.partial_queue.*
import kotlinx.android.synthetic.main.partial_queue.view.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class QueueFragment : BottomSheetDialogFragment() {
private var adapter: TracksAdapter? = null
private val viewModel by viewModel<QueueViewModel>()
private val favoritesRepository by inject<FavoritesRepository>()
private val viewModel = QueueViewModel.get()
lateinit var favoritesRepository: FavoritesRepository
private var adapter: TracksAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
favoritesRepository = FavoritesRepository(context)
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_FloatingBottomSheet)
}

View File

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

View File

@ -6,9 +6,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.TracksAdapter
@ -19,7 +17,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.viewmodels.*
import com.github.apognu.otter.viewmodels.TracksViewModel
import com.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
@ -30,19 +28,22 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
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>() {
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 recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository
lateinit var favoritedRepository: FavoritedRepository
private val favoritesRepository by inject<FavoritesRepository>()
private var albumId = 0
private var albumArtist = ""
private var albumTitle = ""
private var albumCover = ""
companion object {
fun new(album: Album): TracksFragment {
@ -53,35 +54,12 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.apply {
albumId = getInt("albumId")
}
liveData = TracksViewModel(albumId).tracks
AlbumViewModel(albumId).album.observe(this) {
title.text = it.title
Picasso.get()
.maybeLoad(maybeNormalizeUrl(it.cover))
.noFade()
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
.into(cover)
ArtistViewModel(it.artist_id).artist.observe(this) {
artist.text = it.name
}
}
super.onCreate(savedInstanceState)
adapter = TracksAdapter(context, FavoriteListener())
repository = TracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context)
favoritedRepository = FavoritedRepository(context)
watchEventBus()
}
@ -156,6 +134,21 @@ class TracksFragment : LiveOtterFragment<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() {
lifecycleScope.launch(IO) {
EventBus.get().collect { message ->

View File

@ -21,7 +21,7 @@ class OtterResponseSerializer<T : Any>(private val dataSerializer: KSerializer<T
}
@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
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.google.android.exoplayer2.offline.Download
import kotlinx.serialization.ContextualSerialization
import kotlinx.serialization.Serializable
@Serializable
@ -55,10 +56,12 @@ data class FunkwhaleTrack(
@Serializable
data class Favorited(val track: Int)
@Serializable
data class DownloadInfo(
val id: Int,
val contentId: String,
val title: String,
val artist: String,
@ContextualSerialization
var download: Download?
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,45 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.Otter
import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.dao.DecoratedArtistEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import com.github.apognu.otter.models.dao.toRealmDao
import io.realm.Realm
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class ArtistsRepository(override val context: Context?) : Repository<FunkwhaleArtist>() {
class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleArtist>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
scope.launch(IO) {
data.forEach { artist ->
Otter.get().database.artists().insert(artist.toDao())
database.artists().insert(artist.toDao())
Realm.getDefaultInstance().executeTransaction { realm ->
realm.insertOrUpdate(artist.toRealmDao())
}
artist.albums?.forEach { album ->
Otter.get().database.albums().insert(album.toDao(artist.id))
database.albums().insert(album.toDao(artist.id))
}
}
}
return super.onDataFetched(data)
}
fun all(): LiveData<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
import android.content.Context
import com.github.apognu.otter.Otter
import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.Favorited
import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.FavoriteEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Settings
import com.github.apognu.otter.utils.mustNormalizeUrl
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.stringify
class FavoritesRepository(override val context: Context?) : Repository<FunkwhaleTrack>() {
class FavoritesRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleTrack>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", FunkwhaleTrack.serializer())
val favoritedRepository = FavoritedRepository(context)
val favoritedRepository = FavoritedRepository(context, database)
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach {
Otter.get().database.tracks().insertWithAssocs(it)
Otter.get().database.favorites().insert(FavoriteEntity(it.id))
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it)
database.favorites().insert(FavoriteEntity(it.id))
}
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
@ -45,8 +47,18 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
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) {
Otter.get().database.favorites().add(id)
database.favorites().add(id)
val body = mapOf("track" to id)
@ -59,7 +71,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
scope.launch(IO) {
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.body(AppContext.json.stringify(body))
.awaitByteArrayResponseResult()
favoritedRepository.update()
@ -67,7 +79,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
}
fun deleteFavorite(id: Int) = scope.launch(IO) {
Otter.get().database.favorites().remove(id)
database.favorites().remove(id)
val body = mapOf("track" to id)
@ -80,7 +92,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Funkwhale
scope.launch(IO) {
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.body(AppContext.json.stringify(body))
.awaitByteArrayResponseResult()
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 =
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", Favorited.serializer())
override fun onDataFetched(data: List<Favorited>): List<Favorited> {
scope.launch(IO) {
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 ->
"GET $url".log()
error.log()
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> send(Repository.Response(listOf(), page, false))

View File

@ -1,24 +1,35 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.Otter
import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhalePlaylistTrack
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.PlaylistTrack
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class PlaylistTracksRepository(override val context: Context?, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() {
class PlaylistTracksRepository(override val context: Context?, private val database: OtterDatabase, private val playlistId: Int) : Repository<FunkwhalePlaylistTrack>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", FunkwhalePlaylistTrack.serializer())
override fun onDataFetched(data: List<FunkwhalePlaylistTrack>): List<FunkwhalePlaylistTrack> = runBlocking {
Otter.get().database.playlists().replaceTracks(playlistId, data.map {
Otter.get().database.tracks().insertWithAssocs(it.track)
database.playlists().replaceTracks(playlistId, data.map {
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), it.track)
PlaylistTrack(playlistId, it.track.id)
})
data
}
fun tracks(id: Int): LiveData<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
import android.content.Context
import com.github.apognu.otter.Otter
import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhalePlaylist
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.PlaylistEntity
import com.github.apognu.otter.models.dao.toDao
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class PlaylistsRepository(override val context: Context?) : Repository<FunkwhalePlaylist>() {
class PlaylistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhalePlaylist>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", FunkwhalePlaylist.serializer())
override fun onDataFetched(data: List<FunkwhalePlaylist>): List<FunkwhalePlaylist> {
data.forEach {
Otter.get().database.playlists().insert(it.toDao())
database.playlists().insert(it.toDao())
}
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
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.domain.Track
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class QueueRepository(val scope: CoroutineScope) {
fun all() = Otter.get().database.queue().allDecorated()
class QueueRepository(private val database: OtterDatabase, private val scope: CoroutineScope) {
fun all() = database.queue().allDecorated()
fun allBlocking() = Otter.get().database.queue().allDecoratedBlocking()
fun allBlocking() = database.queue().allDecoratedBlocking()
fun replace(tracks: List<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
import android.content.Context
import com.github.apognu.otter.Otter
import androidx.lifecycle.LiveData
import com.github.apognu.otter.models.api.FunkwhaleRadio
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.RadioEntity
import com.github.apognu.otter.models.dao.toDao
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class RadiosRepository(override val context: Context?) : Repository<FunkwhaleRadio>() {
class RadiosRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleRadio>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?playable=true&ordering=name", FunkwhaleRadio.serializer())
override fun onDataFetched(data: List<FunkwhaleRadio>): List<FunkwhaleRadio> {
data.forEach {
Otter.get().database.radios().insert(it.toDao())
database.radios().insert(it.apply { radio_type = "custom" }.toDao())
}
return data
.map { radio -> radio.apply { radio_type = "custom" } }
.toMutableList()
}
fun all(): LiveData<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.FunkwhaleArtist
import com.github.apognu.otter.models.api.FunkwhaleTrack
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<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())
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
val favorites = FavoritedRepository(context).fetch()
.map { it.data }
.toList()
.flatten()
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
data.map { track ->

View File

@ -1,13 +1,21 @@
package com.github.apognu.otter.repositories
import android.content.Context
import androidx.lifecycle.LiveData
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.utils.getMetadata
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.google.android.exoplayer2.offline.Download
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class TracksRepository(override val context: Context?, albumId: Int) : Repository<FunkwhaleTrack>() {
class TracksRepository(override val context: Context, private val database: OtterDatabase, albumId: Int?) : Repository<FunkwhaleTrack>() {
override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer())
@ -32,9 +40,42 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach { track ->
Otter.get().database.tracks().insertWithAssocs(track)
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
}
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
}
fun find(ids: List<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 TRANSITION_DURATION = 300L
val json = Json(JsonConfiguration(ignoreUnknownKeys = true))
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() =
kotlinxDeserializerOf(T::class.serializer(), Json(JsonConfiguration(ignoreUnknownKeys = true)))
kotlinxDeserializerOf(T::class.serializer(), json)
fun init(context: Activity) {
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.kittinunf.fuel.core.Request
import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import kotlinx.coroutines.CoroutineScope
@ -77,4 +76,4 @@ fun Request.authorize(): Request {
}
}
fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java)
fun Download.getMetadata(): DownloadInfo? = AppContext.json.parse(DownloadInfo.serializer(), String(this.request.data))

View File

@ -3,19 +3,19 @@ package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.models.domain.Upload
import com.github.apognu.otter.repositories.AlbumsRepository
import com.github.apognu.otter.repositories.TracksRepository
class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() {
class AlbumsViewModel(private val repository: AlbumsRepository, private val tracksRepository: TracksRepository, private val artistId: Int? = null) : ViewModel() {
val albums: LiveData<List<Album>> by lazy {
if (artistId == null) {
Transformations.map(Otter.get().database.albums().allDecorated()) {
Transformations.map(repository.all()) {
it.map { album -> Album.fromDecoratedEntity(album) }
}
} else {
Transformations.map(Otter.get().database.albums().forArtistDecorated(artistId)) {
Transformations.map(repository.ofArtist(artistId)) {
it.map { album -> Album.fromDecoratedEntity(album) }
}
}
@ -23,24 +23,13 @@ class AlbumsViewModel(private val artistId: Int? = null) : ViewModel() {
suspend fun tracks(): List<Track> {
artistId?.let {
val tracks = Otter.get().database.tracks().ofArtistBlocking(artistId)
val uploads = Otter.get().database.uploads().findAllBlocking(tracks.map { it.id })
val tracks = tracksRepository.ofArtistBlocking(artistId)
return tracks.map {
Track.fromDecoratedEntity(it).apply {
this.uploads = uploads.filter { it.track_id == id }.map { Upload.fromEntity(it) }
}
Track.fromDecoratedEntity(it)
}
}
return listOf()
}
}
class AlbumViewModel(private val id: Int) : ViewModel() {
val album: LiveData<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
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations.map
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository
class ArtistsViewModel : ViewModel() {
companion object {
private lateinit var instance: ArtistsViewModel
fun get(): ArtistsViewModel {
instance = if (::instance.isInitialized) instance else ArtistsViewModel()
return instance
}
}
val artists: LiveData<List<Artist>> = Otter.get().database.artists().allDecorated().map {
it.map { Artist.fromDecoratedEntity(it) }
class ArtistsViewModel(private val repository: ArtistsRepository) : ViewModel() {
val artists: LiveData<List<Artist>> = repository.all().map { artists ->
artists.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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter
import androidx.lifecycle.*
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.models.domain.Upload
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository
import kotlinx.coroutines.delay
class FavoritesViewModel : ViewModel() {
companion object {
private lateinit var instance: FavoritesViewModel
fun get(): FavoritesViewModel {
instance = if (::instance.isInitialized) instance else FavoritesViewModel()
return instance
class FavoritesViewModel(private val repository: FavoritesRepository, private val tracksRepository: TracksRepository) : ViewModel() {
private val _downloaded = liveData {
while (true) {
emit(tracksRepository.downloaded())
delay(5000)
}
}
@ -24,49 +19,40 @@ class FavoritesViewModel : ViewModel() {
Transformations.switchMap(_favorites) { tracks ->
val ids = tracks.mapNotNull { it.album?.id }
Transformations.map(Otter.get().database.albums().findAllDecorated(ids)) { albums ->
Transformations.map(repository.find(ids)) { albums ->
albums.map { album -> Album.fromDecoratedEntity(album) }
}
}
}
private val _favorites: LiveData<List<Track>> by lazy {
Transformations.switchMap(Otter.get().database.favorites().all()) {
Transformations.switchMap(repository.all()) {
val ids = it.map { favorite -> favorite.track_id }
Transformations.map(Otter.get().database.tracks().findAllDecorated(ids)) { tracks ->
Transformations.map(tracksRepository.find(ids)) { tracks ->
tracks.map { track -> Track.fromDecoratedEntity(track) }.sortedBy { it.title }
}
}
}
private val _uploads: LiveData<List<Upload>> by lazy {
Transformations.switchMap(_favorites) { tracks ->
val ids = tracks.mapNotNull { it.album?.id }
Transformations.map(Otter.get().database.uploads().findAll(ids)) { uploads ->
uploads.map { upload -> Upload.fromEntity(upload) }
}
}
}
val favorites = MediatorLiveData<List<Track>>().apply {
addSource(_favorites) { merge(_favorites, _albums, _uploads) }
addSource(_albums) { merge(_favorites, _albums, _uploads) }
addSource(_uploads) { merge(_favorites, _albums, _uploads) }
addSource(_favorites) { merge(_favorites, _albums, _downloaded) }
addSource(_albums) { merge(_favorites, _albums, _downloaded) }
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 _albums = _albums.value
val _uploads = _uploads.value
val _downloaded = _downloaded.value
if (_tracks == null || _albums == null || _uploads == null) {
if (_tracks == null || _albums == null || _downloaded == null) {
return
}
favorites.value = _tracks.map { track ->
track.uploads = _uploads.filter { upload -> upload.track_id == track.id }
track.cached = tracksRepository.isCached(track)
track.downloaded = _downloaded.contains(track.id)
track
}
}

View File

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

View File

@ -1,20 +1,53 @@
package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import com.github.apognu.otter.Otter
import androidx.lifecycle.*
import com.github.apognu.otter.models.dao.PlaylistEntity
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.repositories.PlaylistTracksRepository
import com.github.apognu.otter.repositories.PlaylistsRepository
import com.github.apognu.otter.repositories.TracksRepository
import kotlinx.coroutines.delay
class PlaylistsViewModel : ViewModel() {
val playlists: LiveData<List<PlaylistEntity>> by lazy { Otter.get().database.playlists().all() }
class PlaylistsViewModel(private val repository: PlaylistsRepository) : ViewModel() {
val playlists: LiveData<List<PlaylistEntity>> by lazy { repository.all() }
}
class PlaylistViewModel(playlistId: Int) : ViewModel() {
val tracks: LiveData<List<Track>> by lazy {
Transformations.map(Otter.get().database.playlists().tracksFor(playlistId)) {
class PlaylistViewModel(private val repository: PlaylistTracksRepository, private val tracksRepository: TracksRepository, playerViewModel: PlayerStateViewModel, playlistId: Int) : ViewModel() {
private val _downloaded = liveData {
while (true) {
emit(tracksRepository.downloaded())
delay(5000)
}
}
private val _current = playerViewModel.track
private val _tracks: LiveData<List<Track>> by lazy {
Transformations.map(repository.tracks(playlistId)) {
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 kotlinx.coroutines.delay
class QueueViewModel private constructor() : ViewModel() {
companion object {
private lateinit var instance: QueueViewModel
fun get(): QueueViewModel {
instance = if (::instance.isInitialized) instance else QueueViewModel()
return instance
}
}
private val queueRepository = QueueRepository(viewModelScope)
class QueueViewModel(private val repository: QueueRepository, playerViewModel: PlayerStateViewModel) : ViewModel() {
private val _cached = liveData {
while (true) {
emit(Otter.get().exoCache.keys)
@ -27,10 +15,10 @@ class QueueViewModel private constructor() : ViewModel() {
}
}
private val _current = PlayerStateViewModel.get().track
private val _current = playerViewModel.track
private val _queue: LiveData<List<Track>> by lazy {
Transformations.map(queueRepository.all()) { tracks ->
Transformations.map(repository.all()) { tracks ->
tracks.map { Track.fromDecoratedEntity(it) }
}
}

View File

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

View File

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