Update.
This commit is contained in:
parent
945f227ace
commit
1126d47a1a
|
@ -2,12 +2,14 @@ package com.github.apognu.otter
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.widget.SearchView
|
||||||
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.MainActivity
|
||||||
import com.github.apognu.otter.activities.SearchActivity
|
import com.github.apognu.otter.activities.SearchActivity
|
||||||
import com.github.apognu.otter.adapters.*
|
import com.github.apognu.otter.adapters.*
|
||||||
import com.github.apognu.otter.fragments.*
|
import com.github.apognu.otter.fragments.*
|
||||||
|
import com.github.apognu.otter.models.Mediator
|
||||||
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.Companion.factory
|
import com.github.apognu.otter.playback.QueueManager.Companion.factory
|
||||||
|
@ -105,9 +107,6 @@ class Otter : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
factory { MainActivity() }
|
|
||||||
factory { SearchActivity(get(), get()) }
|
|
||||||
|
|
||||||
fragment { BrowseFragment() }
|
fragment { BrowseFragment() }
|
||||||
fragment { LandscapeQueueFragment() }
|
fragment { LandscapeQueueFragment() }
|
||||||
|
|
||||||
|
@ -115,7 +114,7 @@ class Otter : Application() {
|
||||||
|
|
||||||
single { ArtistsRepository(get(), get()) }
|
single { ArtistsRepository(get(), get()) }
|
||||||
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
|
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
|
||||||
viewModel { ArtistsViewModel(get()) }
|
viewModel { ArtistsViewModel(get(), get()) }
|
||||||
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
|
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
|
||||||
|
|
||||||
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
|
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
|
||||||
|
@ -145,6 +144,13 @@ class Otter : Application() {
|
||||||
|
|
||||||
single { (scope: CoroutineScope) -> QueueRepository(get(), scope) }
|
single { (scope: CoroutineScope) -> QueueRepository(get(), scope) }
|
||||||
viewModel { QueueViewModel(get(), get()) }
|
viewModel { QueueViewModel(get(), get()) }
|
||||||
|
|
||||||
|
viewModel { SearchViewModel(get(), get(), get()) }
|
||||||
|
single { ArtistsSearchRepository(get(), get()) }
|
||||||
|
single { AlbumsSearchRepository(get(), get { parametersOf(null) }) }
|
||||||
|
single { TracksSearchRepository(get(), get { parametersOf(null) }) }
|
||||||
|
|
||||||
|
single { Mediator(get(), get(), get()) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,40 +4,30 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import 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.OtterDatabase
|
||||||
import com.github.apognu.otter.models.dao.toDao
|
|
||||||
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.repositories.AlbumsSearchRepository
|
|
||||||
import com.github.apognu.otter.repositories.ArtistsSearchRepository
|
|
||||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||||
import com.github.apognu.otter.repositories.TracksSearchRepository
|
import com.github.apognu.otter.viewmodels.SearchViewModel
|
||||||
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.android.synthetic.main.activity_search.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class SearchActivity(private val database: OtterDatabase, private val favoritesRepository: FavoritesRepository) : AppCompatActivity() {
|
class SearchActivity : AppCompatActivity() {
|
||||||
|
private val viewModel by viewModel<SearchViewModel>()
|
||||||
|
private val favoritesRepository by inject<FavoritesRepository>()
|
||||||
|
|
||||||
private lateinit var adapter: SearchAdapter
|
private lateinit var adapter: SearchAdapter
|
||||||
|
|
||||||
lateinit var artistsRepository: ArtistsSearchRepository
|
|
||||||
lateinit var albumsRepository: AlbumsSearchRepository
|
|
||||||
lateinit var tracksRepository: TracksSearchRepository
|
|
||||||
|
|
||||||
var done = 0
|
var done = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -51,23 +41,41 @@ class SearchActivity(private val database: OtterDatabase, private val favoritesR
|
||||||
}
|
}
|
||||||
|
|
||||||
search.requestFocus()
|
search.requestFocus()
|
||||||
|
|
||||||
|
viewModel.artists.observe(this) { artists ->
|
||||||
|
if (adapter.artists.size != artists.size) done++
|
||||||
|
|
||||||
|
adapter.artists = artists.toMutableSet()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.albums.observe(this) { albums ->
|
||||||
|
if (adapter.albums.size != albums.size) done++
|
||||||
|
|
||||||
|
adapter.albums = albums.toMutableSet()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.tracks.observe(this) { tracks ->
|
||||||
|
if (adapter.tracks.size != tracks.size) done++
|
||||||
|
|
||||||
|
adapter.tracks = tracks.toMutableSet()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
lifecycleScope.launch(IO) {
|
|
||||||
EventBus.get().collect { message ->
|
|
||||||
when (message) {
|
|
||||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
artistsRepository = ArtistsSearchRepository(this@SearchActivity, "")
|
|
||||||
albumsRepository = AlbumsSearchRepository(this@SearchActivity, "")
|
|
||||||
tracksRepository = TracksSearchRepository(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 {
|
||||||
search.clearFocus()
|
search.clearFocus()
|
||||||
|
@ -75,68 +83,18 @@ class SearchActivity(private val database: OtterDatabase, private val favoritesR
|
||||||
rawQuery?.let {
|
rawQuery?.let {
|
||||||
done = 0
|
done = 0
|
||||||
|
|
||||||
val query = URLEncoder.encode(it, "UTF-8")
|
|
||||||
|
|
||||||
artistsRepository.query = query.toLowerCase(Locale.ROOT)
|
|
||||||
albumsRepository.query = query.toLowerCase(Locale.ROOT)
|
|
||||||
tracksRepository.query = query.toLowerCase(Locale.ROOT)
|
|
||||||
|
|
||||||
search_spinner.visibility = View.VISIBLE
|
|
||||||
search_empty.visibility = View.GONE
|
|
||||||
search_no_results.visibility = View.GONE
|
|
||||||
|
|
||||||
adapter.artists.clear()
|
adapter.artists.clear()
|
||||||
adapter.albums.clear()
|
adapter.albums.clear()
|
||||||
adapter.tracks.clear()
|
adapter.tracks.clear()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|
||||||
artistsRepository.fetch().untilNetwork(lifecycleScope, IO) { artists, _, _ ->
|
val query = URLEncoder.encode(it, "UTF-8")
|
||||||
done++
|
|
||||||
|
|
||||||
artists.forEach {
|
viewModel.search(query)
|
||||||
database.artists().run {
|
|
||||||
insert(it.toDao())
|
|
||||||
|
|
||||||
adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
search_spinner.visibility = View.VISIBLE
|
||||||
}
|
search_empty.visibility = View.GONE
|
||||||
}
|
search_no_results.visibility = View.GONE
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
albumsRepository.fetch().untilNetwork(lifecycleScope, IO) { albums, _, _ ->
|
|
||||||
done++
|
|
||||||
|
|
||||||
albums.forEach {
|
|
||||||
database.albums().run {
|
|
||||||
insert(it.toDao())
|
|
||||||
|
|
||||||
adapter.albums.add(Album.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracksRepository.fetch().untilNetwork(lifecycleScope, IO) { tracks, _, _ ->
|
|
||||||
done++
|
|
||||||
|
|
||||||
tracks.forEach {
|
|
||||||
database.tracks().run {
|
|
||||||
insertWithAssocs(database.artists(), database.albums(), database.uploads(), it)
|
|
||||||
|
|
||||||
adapter.tracks.add(Track.fromDecoratedEntity(getDecoratedBlocking(it.id)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -160,19 +118,6 @@ class SearchActivity(private val database: OtterDatabase, private val favoritesR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
|
||||||
/* download.getMetadata()?.let { info ->
|
|
||||||
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
adapter.tracks[match.second].downloaded = true
|
|
||||||
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
|
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
|
||||||
override fun onArtistClick(holder: View?, artist: Artist) {
|
override fun onArtistClick(holder: View?, artist: Artist) {
|
||||||
ArtistsFragment.openAlbums(this@SearchActivity, artist)
|
ArtistsFragment.openAlbums(this@SearchActivity, artist)
|
||||||
|
|
|
@ -4,24 +4,30 @@ import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
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.models.domain.Artist
|
||||||
import com.github.apognu.otter.utils.maybeLoad
|
import com.github.apognu.otter.utils.maybeLoad
|
||||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
import com.github.apognu.otter.models.domain.Artist
|
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
import kotlinx.android.synthetic.main.row_artist.view.*
|
import kotlinx.android.synthetic.main.row_artist.view.*
|
||||||
|
|
||||||
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
val DIFF = object : DiffUtil.ItemCallback<Artist>() {
|
||||||
|
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = oldItem == newItem
|
||||||
|
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) = oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : PagingDataAdapter<Artist, ArtistsAdapter.ViewHolder>(DIFF) {
|
||||||
interface OnArtistClickListener {
|
interface OnArtistClickListener {
|
||||||
fun onClick(holder: View?, artist: Artist)
|
fun onClick(holder: View?, artist: Artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = data.size
|
// override fun getItemCount() = data.size
|
||||||
|
|
||||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
// override fun getItemId(position: Int) = data[position].id.toLong()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
|
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
|
||||||
|
@ -32,7 +38,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val artist = data[position]
|
val artist = getItem(position)!!
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
.maybeLoad(maybeNormalizeUrl(artist.album_cover))
|
.maybeLoad(maybeNormalizeUrl(artist.album_cover))
|
||||||
|
@ -50,7 +56,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
||||||
val albums = view.albums
|
val albums = view.albums
|
||||||
|
|
||||||
override fun onClick(view: View?) {
|
override fun onClick(view: View?) {
|
||||||
data[layoutPosition].let { artist ->
|
getItem(layoutPosition)?.let { artist ->
|
||||||
listener.onClick(view, artist)
|
listener.onClick(view, artist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.models.dao.PlaylistEntity
|
import com.github.apognu.otter.models.dao.PlaylistEntity
|
||||||
import com.github.apognu.otter.fragments.OtterAdapter
|
import com.github.apognu.otter.fragments.OtterAdapter
|
||||||
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
import com.github.apognu.otter.utils.toDurationString
|
import com.github.apognu.otter.utils.toDurationString
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
|
@ -54,7 +55,7 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
|
||||||
}
|
}
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
.load(url)
|
.load(maybeNormalizeUrl(url))
|
||||||
.transform(RoundedCornersTransformation(32, 0, corner))
|
.transform(RoundedCornersTransformation(32, 0, corner))
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.apognu.otter.models.domain.Album
|
import com.github.apognu.otter.models.domain.Album
|
||||||
import com.github.apognu.otter.models.domain.Artist
|
import com.github.apognu.otter.models.domain.Artist
|
||||||
|
@ -41,11 +40,9 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||||
|
|
||||||
val SECTION_COUNT = 3
|
val SECTION_COUNT = 3
|
||||||
|
|
||||||
var artists: MutableList<Artist> = mutableListOf()
|
var artists: MutableSet<Artist> = mutableSetOf()
|
||||||
var albums: MutableList<Album> = mutableListOf()
|
var albums: MutableSet<Album> = mutableSetOf()
|
||||||
var tracks: MutableList<Track> = mutableListOf()
|
var tracks: MutableSet<Track> = mutableSetOf()
|
||||||
|
|
||||||
var currentTrack: FunkwhaleTrack? = null
|
|
||||||
|
|
||||||
override fun getItemCount() = SECTION_COUNT + artists.size + albums.size + tracks.size
|
override fun getItemCount() = SECTION_COUNT + artists.size + albums.size + tracks.size
|
||||||
|
|
||||||
|
@ -57,9 +54,9 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||||
return -3
|
return -3
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultType.Artist.ordinal -> artists[position].id.toLong()
|
ResultType.Artist.ordinal -> artists.elementAt(position).id.toLong()
|
||||||
ResultType.Artist.ordinal -> albums[position - artists.size - 2].id.toLong()
|
ResultType.Artist.ordinal -> albums.elementAt(position - artists.size - 2).id.toLong()
|
||||||
ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - SECTION_COUNT].id.toLong()
|
ResultType.Track.ordinal -> tracks.elementAt(position - artists.size - albums.size - SECTION_COUNT).id.toLong()
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,19 +131,19 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||||
holder.actions.visibility = View.GONE
|
holder.actions.visibility = View.GONE
|
||||||
holder.favorite.visibility = View.GONE
|
holder.favorite.visibility = View.GONE
|
||||||
|
|
||||||
artists[position - 1]
|
artists.elementAt(position - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultType.Album.ordinal -> {
|
ResultType.Album.ordinal -> {
|
||||||
holder.actions.visibility = View.GONE
|
holder.actions.visibility = View.GONE
|
||||||
holder.favorite.visibility = View.GONE
|
holder.favorite.visibility = View.GONE
|
||||||
|
|
||||||
albums[position - artists.size - 2]
|
albums.elementAt(position - artists.size - 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - SECTION_COUNT]
|
ResultType.Track.ordinal -> tracks.elementAt(position - artists.size - albums.size - SECTION_COUNT)
|
||||||
|
|
||||||
else -> tracks[position]
|
else -> tracks.elementAt(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
|
@ -173,10 +170,11 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||||
if (resultType == ResultType.Track.ordinal) {
|
if (resultType == ResultType.Track.ordinal) {
|
||||||
(item as? Track)?.let { track ->
|
(item as? Track)?.let { track ->
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
/* if (track == currentTrack || track.current) {
|
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
||||||
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
|
|
||||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
|
if (track.current) {
|
||||||
} */
|
holder.itemView.background = context.getDrawable(R.drawable.current)
|
||||||
|
}
|
||||||
|
|
||||||
when (track.favorite) {
|
when (track.favorite) {
|
||||||
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
|
||||||
|
@ -250,19 +248,19 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
||||||
ResultType.Artist.ordinal -> {
|
ResultType.Artist.ordinal -> {
|
||||||
val position = layoutPosition - 1
|
val position = layoutPosition - 1
|
||||||
|
|
||||||
listener?.onArtistClick(view, artists[position])
|
listener?.onArtistClick(view, artists.elementAt(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultType.Album.ordinal -> {
|
ResultType.Album.ordinal -> {
|
||||||
val position = layoutPosition - artists.size - 2
|
val position = layoutPosition - artists.size - 2
|
||||||
|
|
||||||
listener?.onAlbumClick(view, albums[position])
|
listener?.onAlbumClick(view, albums.elementAt(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultType.Track.ordinal -> {
|
ResultType.Track.ordinal -> {
|
||||||
val position = layoutPosition - artists.size - albums.size - SECTION_COUNT
|
val position = layoutPosition - artists.size - albums.size - SECTION_COUNT
|
||||||
|
|
||||||
tracks.subList(position, tracks.size).plus(tracks.subList(0, position)).apply {
|
tracks.toList().subList(position, tracks.size).plus(tracks.toList().subList(0, position)).apply {
|
||||||
CommandBus.send(Command.ReplaceQueue(this))
|
CommandBus.send(Command.ReplaceQueue(this))
|
||||||
|
|
||||||
context.toast("All tracks were added to your queue")
|
context.toast("All tracks were added to your queue")
|
||||||
|
|
|
@ -32,7 +32,7 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
|
class AlbumsFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
|
||||||
override val repository by inject<AlbumsRepository> { parametersOf(null) }
|
override val repository by inject<AlbumsRepository> { parametersOf(null) }
|
||||||
override val adapter by inject<AlbumsAdapter> { parametersOf(context, OnAlbumClickListener()) }
|
override val adapter by inject<AlbumsAdapter> { parametersOf(context, OnAlbumClickListener()) }
|
||||||
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(artistId) }
|
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(artistId) }
|
||||||
|
|
|
@ -19,7 +19,7 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
|
class AlbumsGridFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
|
||||||
override val repository by inject<AlbumsRepository> { parametersOf(null) }
|
override val repository by inject<AlbumsRepository> { parametersOf(null) }
|
||||||
override val adapter by inject<AlbumsGridAdapter> { parametersOf(context, OnAlbumClickListener()) }
|
override val adapter by inject<AlbumsGridAdapter> { parametersOf(context, OnAlbumClickListener()) }
|
||||||
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(null) }
|
override val viewModel by viewModel<AlbumsViewModel> { parametersOf(null) }
|
||||||
|
@ -30,6 +30,8 @@ class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAd
|
||||||
override val layoutManager get() = GridLayoutManager(context, 3)
|
override val layoutManager get() = GridLayoutManager(context, 3)
|
||||||
override val alwaysRefresh = false
|
override val alwaysRefresh = false
|
||||||
|
|
||||||
|
override val OFFSCREEN_PAGES = 5
|
||||||
|
|
||||||
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 ->
|
||||||
|
|
|
@ -28,12 +28,12 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
|
class ArtistsFragment : PagedOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
|
||||||
override val repository by inject<ArtistsRepository>()
|
override val repository by inject<ArtistsRepository>()
|
||||||
override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) }
|
override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) }
|
||||||
override val viewModel by viewModel<ArtistsViewModel>()
|
override val viewModel by viewModel<ArtistsViewModel>()
|
||||||
|
|
||||||
override val liveData by lazy { viewModel.artists }
|
override val liveData by lazy { viewModel.artistsPaged }
|
||||||
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
|
||||||
|
|
||||||
|
@ -77,21 +77,6 @@ class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapte
|
||||||
return inflater.inflate(R.layout.fragment_artists, container, false)
|
return inflater.inflate(R.layout.fragment_artists, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
artists.layoutManager = LinearLayoutManager(context)
|
|
||||||
(artists.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
|
||||||
artists.adapter = adapter
|
|
||||||
|
|
||||||
liveData.observe(viewLifecycleOwner) { result ->
|
|
||||||
adapter.data.size.let { position ->
|
|
||||||
adapter.data = result.toMutableList()
|
|
||||||
adapter.notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
|
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
|
||||||
override fun onClick(holder: View?, artist: Artist) {
|
override fun onClick(holder: View?, artist: Artist) {
|
||||||
openAlbums(context, artist, this@ArtistsFragment, artist.album_cover)
|
openAlbums(context, artist, this@ArtistsFragment, artist.album_cover)
|
||||||
|
|
|
@ -23,7 +23,7 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class FavoritesFragment : LiveOtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
|
class FavoritesFragment : OtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
|
||||||
override val repository by inject<FavoritesRepository>()
|
override val repository by inject<FavoritesRepository>()
|
||||||
override val adapter by inject<FavoritesAdapter> { parametersOf(context, FavoriteListener()) }
|
override val adapter by inject<FavoritesAdapter> { parametersOf(context, FavoriteListener()) }
|
||||||
override val viewModel by viewModel<FavoritesViewModel>()
|
override val viewModel by viewModel<FavoritesViewModel>()
|
||||||
|
|
|
@ -9,6 +9,8 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.observe
|
import androidx.lifecycle.observe
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
@ -16,14 +18,14 @@ import com.github.apognu.otter.repositories.HttpUpstream
|
||||||
import com.github.apognu.otter.repositories.Repository
|
import com.github.apognu.otter.repositories.Repository
|
||||||
import com.github.apognu.otter.utils.Event
|
import com.github.apognu.otter.utils.Event
|
||||||
import com.github.apognu.otter.utils.EventBus
|
import com.github.apognu.otter.utils.EventBus
|
||||||
|
import com.github.apognu.otter.utils.log
|
||||||
import com.github.apognu.otter.utils.untilNetwork
|
import com.github.apognu.otter.utils.untilNetwork
|
||||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||||
|
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.Job
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import org.koin.ext.scope
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||||
var data: MutableList<D> = mutableListOf()
|
var data: MutableList<D> = mutableListOf()
|
||||||
|
@ -35,10 +37,8 @@ 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<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
abstract class OtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||||
companion object {
|
open val OFFSCREEN_PAGES = 10
|
||||||
const val OFFSCREEN_PAGES = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract val repository: Repository<DAO>
|
abstract val repository: Repository<DAO>
|
||||||
abstract val adapter: A
|
abstract val adapter: A
|
||||||
|
@ -152,3 +152,82 @@ abstract class LiveOtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : F
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *>> : Fragment() {
|
||||||
|
open val OFFSCREEN_PAGES = 10
|
||||||
|
|
||||||
|
abstract val repository: Repository<DAO>
|
||||||
|
abstract val adapter: A
|
||||||
|
open val viewModel: ViewModel? = null
|
||||||
|
|
||||||
|
abstract val liveData: LiveData<PagingData<D>>
|
||||||
|
abstract val viewRes: Int
|
||||||
|
abstract val recycler: RecyclerView
|
||||||
|
|
||||||
|
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
|
||||||
|
open val alwaysRefresh = true
|
||||||
|
|
||||||
|
private var moreLoading = false
|
||||||
|
private var listener: Job? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(viewRes, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
liveData.observe(viewLifecycleOwner) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||||
|
adapter.submitData(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler.layoutManager = layoutManager
|
||||||
|
recycler.adapter = adapter
|
||||||
|
|
||||||
|
if (listener == null) {
|
||||||
|
listener = lifecycleScope.launch(IO) {
|
||||||
|
EventBus.get().collect { event ->
|
||||||
|
if (event is Event.ListingsChanged) {
|
||||||
|
withContext(Main) {
|
||||||
|
swiper?.isRefreshing = true
|
||||||
|
fetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onDataFetched(data: List<DAO>) {}
|
||||||
|
|
||||||
|
private fun fetch(size: Int = 0) {
|
||||||
|
moreLoading = true
|
||||||
|
|
||||||
|
repository.fetch(size).untilNetwork(lifecycleScope, IO) { data, _, hasMore ->
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
onDataFetched(data)
|
||||||
|
|
||||||
|
(repository.upstream as? HttpUpstream<*>)?.let { upstream ->
|
||||||
|
when (upstream.behavior) {
|
||||||
|
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
|
||||||
|
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
|
||||||
|
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun needsMoreOffscreenPages(): Boolean {
|
||||||
|
view?.let {
|
||||||
|
val offset = recycler.computeVerticalScrollOffset()
|
||||||
|
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
|
||||||
|
|
||||||
|
return left < (recycler.height * OFFSCREEN_PAGES)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class PlaylistTracksFragment : LiveOtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
|
class PlaylistTracksFragment : OtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
|
||||||
private val favoritesRepository by inject<FavoritesRepository>()
|
private val favoritesRepository by inject<FavoritesRepository>()
|
||||||
override val repository by inject<PlaylistTracksRepository> { parametersOf(playlistId) }
|
override val repository by inject<PlaylistTracksRepository> { parametersOf(playlistId) }
|
||||||
override val adapter by inject<PlaylistTracksAdapter> { parametersOf(context, FavoriteListener()) }
|
override val adapter by inject<PlaylistTracksAdapter> { parametersOf(context, FavoriteListener()) }
|
||||||
|
|
|
@ -19,7 +19,7 @@ import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class PlaylistsFragment : LiveOtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
|
class PlaylistsFragment : OtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
|
||||||
override val repository by inject<PlaylistsRepository>()
|
override val repository by inject<PlaylistsRepository>()
|
||||||
override val adapter by inject<PlaylistsAdapter> { parametersOf(context, OnPlaylistClickListener()) }
|
override val adapter by inject<PlaylistsAdapter> { parametersOf(context, OnPlaylistClickListener()) }
|
||||||
override val viewModel by viewModel<PlaylistsViewModel>()
|
override val viewModel by viewModel<PlaylistsViewModel>()
|
||||||
|
|
|
@ -8,7 +8,10 @@ import com.github.apognu.otter.adapters.RadiosAdapter
|
||||||
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
import com.github.apognu.otter.models.api.FunkwhaleRadio
|
||||||
import com.github.apognu.otter.models.dao.RadioEntity
|
import com.github.apognu.otter.models.dao.RadioEntity
|
||||||
import com.github.apognu.otter.repositories.RadiosRepository
|
import com.github.apognu.otter.repositories.RadiosRepository
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.Command
|
||||||
|
import com.github.apognu.otter.utils.CommandBus
|
||||||
|
import com.github.apognu.otter.utils.Event
|
||||||
|
import com.github.apognu.otter.utils.EventBus
|
||||||
import com.github.apognu.otter.viewmodels.RadiosViewModel
|
import com.github.apognu.otter.viewmodels.RadiosViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_radios.*
|
import kotlinx.android.synthetic.main.fragment_radios.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
@ -17,7 +20,7 @@ import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
|
class RadiosFragment : OtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
|
||||||
override val repository by inject<RadiosRepository>()
|
override val repository by inject<RadiosRepository>()
|
||||||
override val adapter by inject<RadiosAdapter> { parametersOf(context, lifecycleScope, RadioClickListener()) }
|
override val adapter by inject<RadiosAdapter> { parametersOf(context, lifecycleScope, RadioClickListener()) }
|
||||||
override val viewModel by inject<RadiosViewModel>()
|
override val viewModel by inject<RadiosViewModel>()
|
||||||
|
|
|
@ -3,36 +3,32 @@ package com.github.apognu.otter.fragments
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
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.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.adapters.TracksAdapter
|
import com.github.apognu.otter.adapters.TracksAdapter
|
||||||
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
import com.github.apognu.otter.models.api.FunkwhaleTrack
|
||||||
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.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.Command
|
||||||
|
import com.github.apognu.otter.utils.CommandBus
|
||||||
|
import com.github.apognu.otter.utils.maybeLoad
|
||||||
|
import com.github.apognu.otter.utils.toast
|
||||||
import com.github.apognu.otter.viewmodels.TracksViewModel
|
import com.github.apognu.otter.viewmodels.TracksViewModel
|
||||||
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
|
||||||
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.IO
|
|
||||||
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.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>() {
|
class TracksFragment : OtterFragment<FunkwhaleTrack, Track, TracksAdapter>() {
|
||||||
|
private val favoritesRepository by inject<FavoritesRepository>()
|
||||||
|
|
||||||
override val repository by inject<TracksRepository> { parametersOf(albumId) }
|
override val repository by inject<TracksRepository> { parametersOf(albumId) }
|
||||||
override val adapter by inject<TracksAdapter> { parametersOf(context, FavoriteListener()) }
|
override val adapter by inject<TracksAdapter> { parametersOf(context, FavoriteListener()) }
|
||||||
override val viewModel by viewModel<TracksViewModel> { parametersOf(albumId) }
|
override val viewModel by viewModel<TracksViewModel> { parametersOf(albumId) }
|
||||||
|
@ -41,8 +37,6 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
|
||||||
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
|
||||||
|
|
||||||
private val favoritesRepository by inject<FavoritesRepository>()
|
|
||||||
|
|
||||||
private var albumId = 0
|
private var albumId = 0
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -59,8 +53,6 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
albumId = getInt("albumId")
|
albumId = getInt("albumId")
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEventBus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -149,42 +141,6 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun watchEventBus() {
|
|
||||||
lifecycleScope.launch(IO) {
|
|
||||||
EventBus.get().collect { message ->
|
|
||||||
when (message) {
|
|
||||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTracks() {
|
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
|
||||||
|
|
||||||
withContext(Main) {
|
|
||||||
/* adapter.data = adapter.data.map {
|
|
||||||
it.downloaded = downloaded.contains(it.id)
|
|
||||||
it
|
|
||||||
}.toMutableList() */
|
|
||||||
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
|
||||||
download.getMetadata()?.let { info ->
|
|
||||||
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
|
||||||
/* withContext(Main) {
|
|
||||||
adapter.data[match.second].downloaded = true
|
|
||||||
adapter.notifyItemChanged(match.second)
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
inner class FavoriteListener : TracksAdapter.OnFavoriteListener {
|
||||||
override fun onToggleFavorite(id: Int, state: Boolean) {
|
override fun onToggleFavorite(id: Int, state: Boolean) {
|
||||||
when (state) {
|
when (state) {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.github.apognu.otter.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.paging.LoadType
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.paging.RemoteMediator
|
||||||
|
import androidx.room.withTransaction
|
||||||
|
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.domain.Artist
|
||||||
|
import com.github.apognu.otter.repositories.ArtistsRepository
|
||||||
|
import com.github.apognu.otter.utils.AppContext
|
||||||
|
import com.github.apognu.otter.utils.Cache
|
||||||
|
import com.github.apognu.otter.utils.log
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
|
class Mediator(private val context: Context, private val database: OtterDatabase, private val repository: ArtistsRepository) : RemoteMediator<Int, DecoratedArtistEntity>(), KoinComponent {
|
||||||
|
override suspend fun load(loadType: LoadType, state: PagingState<Int, DecoratedArtistEntity>): MediatorResult {
|
||||||
|
return try {
|
||||||
|
val key = when (loadType) {
|
||||||
|
LoadType.REFRESH -> 1
|
||||||
|
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
|
||||||
|
LoadType.APPEND -> {
|
||||||
|
Cache.get(context, "key")?.readLine()?.toInt() ?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key.log("fetching page")
|
||||||
|
|
||||||
|
val response = repository.fetch((key - 1) * AppContext.PAGE_SIZE).take(1).first()
|
||||||
|
|
||||||
|
database.withTransaction {
|
||||||
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
Cache.delete(context, "key")
|
||||||
|
database.artists().deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache.set(context, "key", (key + 1).toString().toByteArray())
|
||||||
|
|
||||||
|
response.data.forEach {
|
||||||
|
database.artists().insert(it.toDao())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = !response.hasMore)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,3 +22,4 @@ data class Covers(val urls: CoverUrls?)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CoverUrls(val original: String?)
|
data class CoverUrls(val original: String?)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||||
data class AlbumEntity(
|
data class AlbumEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
|
||||||
val title: String,
|
val title: String,
|
||||||
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = ForeignKey.CASCADE)
|
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = ForeignKey.CASCADE)
|
||||||
val artist_id: Int,
|
val artist_id: Int,
|
||||||
|
@ -18,13 +19,13 @@ data class AlbumEntity(
|
||||||
|
|
||||||
@androidx.room.Dao
|
@androidx.room.Dao
|
||||||
interface Dao {
|
interface Dao {
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity")
|
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY title")
|
||||||
fun allDecorated(): LiveData<List<DecoratedAlbumEntity>>
|
fun allDecorated(): LiveData<List<DecoratedAlbumEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY release_date")
|
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY title")
|
||||||
fun allSync(): List<DecoratedAlbumEntity>
|
fun allSync(): List<DecoratedAlbumEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id IN ( :ids ) ORDER BY release_date")
|
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id IN ( :ids ) ORDER BY title")
|
||||||
fun findAllDecorated(ids: List<Int>): LiveData<List<DecoratedAlbumEntity>>
|
fun findAllDecorated(ids: List<Int>): LiveData<List<DecoratedAlbumEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
||||||
|
@ -33,7 +34,7 @@ data class AlbumEntity(
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
|
||||||
fun getDecoratedBlocking(id: Int): DecoratedAlbumEntity
|
fun getDecoratedBlocking(id: Int): DecoratedAlbumEntity
|
||||||
|
|
||||||
@Query("SELECT * FROM DecoratedAlbumEntity WHERE artist_id = :artistId")
|
@Query("SELECT * FROM DecoratedAlbumEntity WHERE artist_id = :artistId ORDER BY release_date")
|
||||||
fun forArtistDecorated(artistId: Int): LiveData<List<DecoratedAlbumEntity>>
|
fun forArtistDecorated(artistId: Int): LiveData<List<DecoratedAlbumEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.apognu.otter.models.dao
|
package com.github.apognu.otter.models.dao
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.DataSource
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
import com.github.apognu.otter.models.api.FunkwhaleArtist
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
|
@ -10,12 +11,15 @@ import io.realm.annotations.Required
|
||||||
data class ArtistEntity(
|
data class ArtistEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@ColumnInfo(collate = ColumnInfo.LOCALIZED, index = true)
|
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
|
||||||
val name: String
|
val name: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@androidx.room.Dao
|
@androidx.room.Dao
|
||||||
interface Dao {
|
interface Dao {
|
||||||
|
@Query("SELECT * FROM DecoratedArtistEntity")
|
||||||
|
fun allPaged(): DataSource.Factory<Int, DecoratedArtistEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM DecoratedArtistEntity")
|
@Query("SELECT * FROM DecoratedArtistEntity")
|
||||||
fun allDecorated(): LiveData<List<DecoratedArtistEntity>>
|
fun allDecorated(): LiveData<List<DecoratedArtistEntity>>
|
||||||
|
|
||||||
|
@ -25,6 +29,9 @@ data class ArtistEntity(
|
||||||
@Query("SELECT * FROM DecoratedArtistEntity WHERE id == :id")
|
@Query("SELECT * FROM DecoratedArtistEntity WHERE id == :id")
|
||||||
fun getDecoratedBlocking(id: Int): DecoratedArtistEntity
|
fun getDecoratedBlocking(id: Int): DecoratedArtistEntity
|
||||||
|
|
||||||
|
@Query("SELECT * FROM DecoratedArtistEntity WHERE id IN ( :ids )")
|
||||||
|
fun findDecorated(ids: List<Int>): LiveData<List<DecoratedArtistEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(artist: ArtistEntity)
|
fun insert(artist: ArtistEntity)
|
||||||
|
|
||||||
|
@ -34,6 +41,7 @@ data class ArtistEntity(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FunkwhaleArtist.toDao() = run {
|
fun FunkwhaleArtist.toDao() = run {
|
||||||
|
|
||||||
ArtistEntity(id, name)
|
ArtistEntity(id, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +51,7 @@ fun FunkwhaleArtist.toDao() = run {
|
||||||
INNER JOIN albums
|
INNER JOIN albums
|
||||||
ON albums.artist_id = artists.id
|
ON albums.artist_id = artists.id
|
||||||
GROUP BY albums.artist_id
|
GROUP BY albums.artist_id
|
||||||
ORDER BY name
|
ORDER BY artists.id
|
||||||
""")
|
""")
|
||||||
data class DecoratedArtistEntity(
|
data class DecoratedArtistEntity(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.github.apognu.otter.models.api.FunkwhalePlaylist
|
||||||
data class PlaylistEntity(
|
data class PlaylistEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
|
||||||
val name: String,
|
val name: String,
|
||||||
val album_covers: List<String>,
|
val album_covers: List<String>,
|
||||||
val tracks_count: Int,
|
val tracks_count: Int,
|
||||||
|
|
|
@ -9,6 +9,7 @@ data class RadioEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int,
|
val id: Int,
|
||||||
var radio_type: String?,
|
var radio_type: String?,
|
||||||
|
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
var related_object_id: String? = null
|
var related_object_id: String? = null
|
||||||
|
|
|
@ -3,19 +3,19 @@ package com.github.apognu.otter.models.dao
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import androidx.room.ForeignKey.CASCADE
|
import androidx.room.ForeignKey.CASCADE
|
||||||
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(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
|
||||||
val title: String,
|
val title: String,
|
||||||
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = CASCADE)
|
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = CASCADE)
|
||||||
val artist_id: Int,
|
val artist_id: Int,
|
||||||
@ForeignKey(entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["album_id"], onDelete = CASCADE)
|
@ForeignKey(entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["album_id"], onDelete = CASCADE)
|
||||||
val album_id: Int?,
|
val album_id: Int?,
|
||||||
|
@ColumnInfo(index = true)
|
||||||
val position: Int?,
|
val position: Int?,
|
||||||
val copyright: String?,
|
val copyright: String?,
|
||||||
val license: String?
|
val license: String?
|
||||||
|
|
|
@ -25,13 +25,15 @@ class AlbumsRepository(override val context: Context, private val database: Otte
|
||||||
|
|
||||||
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
|
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
|
||||||
data.forEach {
|
data.forEach {
|
||||||
database.albums().insert(it.toDao())
|
insert(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onDataFetched(data)
|
return super.onDataFetched(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun insert(album: FunkwhaleAlbum) = database.albums().insert(album.toDao())
|
||||||
fun all() = database.albums().allDecorated()
|
fun all() = database.albums().allDecorated()
|
||||||
|
fun find(ids: List<Int>) = database.albums().findAllDecorated(ids)
|
||||||
|
|
||||||
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> {
|
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : 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=id", FunkwhaleArtist.serializer())
|
||||||
|
|
||||||
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
|
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
|
||||||
scope.launch(IO) {
|
scope.launch(IO) {
|
||||||
|
@ -34,6 +34,10 @@ class ArtistsRepository(override val context: Context, private val database: Ott
|
||||||
return super.onDataFetched(data)
|
return super.onDataFetched(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun insert(artist: FunkwhaleArtist) = database.artists().insert(artist.toDao())
|
||||||
|
|
||||||
|
fun allPaged() = database.artists().allPaged()
|
||||||
|
|
||||||
fun all(): LiveData<List<DecoratedArtistEntity>> {
|
fun all(): LiveData<List<DecoratedArtistEntity>> {
|
||||||
scope.launch(IO) {
|
scope.launch(IO) {
|
||||||
fetch().collect()
|
fetch().collect()
|
||||||
|
@ -41,5 +45,7 @@ class ArtistsRepository(override val context: Context, private val database: Ott
|
||||||
|
|
||||||
return database.artists().allDecorated()
|
return database.artists().allDecorated()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(id: Int) = database.artists().getDecorated(id)
|
fun get(id: Int) = database.artists().getDecorated(id)
|
||||||
|
fun find(ids: List<Int>) = database.artists().findDecorated(ids)
|
||||||
}
|
}
|
|
@ -10,10 +10,7 @@ import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
@ -22,8 +19,8 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
|
||||||
Single, AtOnce, Progressive
|
Single, AtOnce, Progressive
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = channelFlow {
|
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
|
||||||
if (behavior == Behavior.Single && size != 0) return@channelFlow
|
if (behavior == Behavior.Single && size != 0) return@flow
|
||||||
|
|
||||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||||
|
|
||||||
|
@ -41,20 +38,22 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
|
||||||
val data = response.results
|
val data = response.results
|
||||||
|
|
||||||
when (behavior) {
|
when (behavior) {
|
||||||
Behavior.Single -> send(Repository.Response(data, page, false))
|
Behavior.Single -> emit(Repository.Response(data, page, false))
|
||||||
Behavior.Progressive -> send(Repository.Response(data, page, response.next != null))
|
Behavior.Progressive -> emit(Repository.Response(data, page, response.next != null))
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
send(Repository.Response(data, page, response.next != null))
|
emit(Repository.Response(data, page, response.next != null))
|
||||||
|
|
||||||
if (response.next != null) fetch(size + data.size).collect { send(it) }
|
if (response.next != null) fetch(size + data.size).collect { emit(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
|
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 -> emit(Repository.Response(listOf(), page, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,41 +1,117 @@
|
||||||
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 androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.lifecycle.map
|
||||||
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.runBlocking
|
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 kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleTrack>() {
|
class ArtistsSearchRepository(override val context: Context?, private val repository: ArtistsRepository, var query: String = "") : Repository<FunkwhaleArtist>() {
|
||||||
override val upstream: Upstream<FunkwhaleTrack>
|
override val upstream: Upstream<FunkwhaleArtist>
|
||||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer())
|
||||||
|
|
||||||
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
|
private val ids: MutableList<Int> = mutableListOf()
|
||||||
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
|
||||||
|
|
||||||
data.map { track ->
|
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
|
||||||
track.favorite = favorites.contains(track.id)
|
|
||||||
track.downloaded = downloaded.contains(track.id)
|
|
||||||
|
|
||||||
track.bestUpload()?.let { upload ->
|
val results: LiveData<List<Artist>> = Transformations.switchMap(_ids) {
|
||||||
val url = mustNormalizeUrl(upload.listen_url)
|
repository.find(it).map { artists -> artists.map { artist -> Artist.fromDecoratedEntity(artist) } }
|
||||||
|
}
|
||||||
|
|
||||||
track.cached = Otter.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
|
||||||
}
|
data.forEach {
|
||||||
|
repository.insert(it)
|
||||||
|
}
|
||||||
|
|
||||||
track
|
ids.addAll(data.map { it.id })
|
||||||
} */
|
_ids.postValue(ids)
|
||||||
|
|
||||||
data
|
return super.onDataFetched(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(term: String) {
|
||||||
|
ids.clear()
|
||||||
|
_ids.postValue(listOf())
|
||||||
|
query = term
|
||||||
|
|
||||||
|
scope.launch(IO) {
|
||||||
|
fetch().collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleArtist>() {
|
class AlbumsSearchRepository(override val context: Context?, private val repository: AlbumsRepository, var query: String = "") : Repository<FunkwhaleAlbum>() {
|
||||||
override val upstream: Upstream<FunkwhaleArtist>
|
|
||||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer())
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<FunkwhaleAlbum>() {
|
|
||||||
override val upstream: Upstream<FunkwhaleAlbum>
|
override val upstream: Upstream<FunkwhaleAlbum>
|
||||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", FunkwhaleAlbum.serializer())
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", FunkwhaleAlbum.serializer())
|
||||||
|
|
||||||
|
private val ids: MutableList<Int> = mutableListOf()
|
||||||
|
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
|
||||||
|
|
||||||
|
val results: LiveData<List<Album>> = Transformations.switchMap(_ids) {
|
||||||
|
repository.find(it).map { albums -> albums.map { album -> Album.fromDecoratedEntity(album) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
|
||||||
|
data.forEach {
|
||||||
|
repository.insert(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids.addAll(data.map { it.id })
|
||||||
|
_ids.postValue(ids)
|
||||||
|
|
||||||
|
return super.onDataFetched(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(term: String) {
|
||||||
|
ids.clear()
|
||||||
|
_ids.postValue(listOf())
|
||||||
|
|
||||||
|
query = term
|
||||||
|
|
||||||
|
scope.launch(IO) {
|
||||||
|
fetch().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TracksSearchRepository(override val context: Context?, private val repository: TracksRepository, var query: String = "") : Repository<FunkwhaleTrack>() {
|
||||||
|
override val upstream: Upstream<FunkwhaleTrack>
|
||||||
|
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
|
||||||
|
|
||||||
|
private val ids: MutableList<Int> = mutableListOf()
|
||||||
|
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
|
||||||
|
|
||||||
|
val results: LiveData<List<Track>> = Transformations.switchMap(_ids) {
|
||||||
|
repository.find(it).map { tracks -> tracks.map { track -> Track.fromDecoratedEntity(track) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> {
|
||||||
|
data.forEach {
|
||||||
|
repository.insert(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids.addAll(data.map { it.id })
|
||||||
|
_ids.postValue(ids)
|
||||||
|
|
||||||
|
return super.onDataFetched(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(term: String) {
|
||||||
|
ids.clear()
|
||||||
|
_ids.postValue(listOf())
|
||||||
|
query = term
|
||||||
|
|
||||||
|
scope.launch(IO) {
|
||||||
|
fetch().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -46,6 +46,10 @@ class TracksRepository(override val context: Context, private val database: Otte
|
||||||
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
data.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun insert(track: FunkwhaleTrack) {
|
||||||
|
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
|
||||||
|
}
|
||||||
|
|
||||||
fun find(ids: List<Int>) = database.tracks().findAllDecorated(ids)
|
fun find(ids: List<Int>) = database.tracks().findAllDecorated(ids)
|
||||||
|
|
||||||
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)
|
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)
|
||||||
|
|
|
@ -39,24 +39,6 @@ object AppContext {
|
||||||
|
|
||||||
fun init(context: Activity) {
|
fun init(context: Activity) {
|
||||||
setupNotificationChannels(context)
|
setupNotificationChannels(context)
|
||||||
|
|
||||||
// CastContext.getSharedInstance(context)
|
|
||||||
|
|
||||||
FuelManager.instance.addResponseInterceptor { next ->
|
|
||||||
{ request, response ->
|
|
||||||
if (request.method == Method.GET && response.statusCode == 200) {
|
|
||||||
var cacheId = request.url.path.toString()
|
|
||||||
|
|
||||||
request.url.query?.let {
|
|
||||||
cacheId = "$cacheId?$it"
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache.set(context, cacheId, response.body().toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
next(request, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
|
|
|
@ -1,12 +1,29 @@
|
||||||
package com.github.apognu.otter.viewmodels
|
package com.github.apognu.otter.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.paging.Pager
|
||||||
import androidx.lifecycle.map
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
|
import com.github.apognu.otter.models.Mediator
|
||||||
import com.github.apognu.otter.models.domain.Artist
|
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 kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class ArtistsViewModel(repository: ArtistsRepository, mediator: Mediator) : ViewModel() {
|
||||||
|
private val pager = Pager(
|
||||||
|
config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = AppContext.PAGE_SIZE * 5, prefetchDistance = 10 * AppContext.PAGE_SIZE, maxSize = 25 * AppContext.PAGE_SIZE, enablePlaceholders = false),
|
||||||
|
pagingSourceFactory = repository.allPaged().asPagingSourceFactory(),
|
||||||
|
remoteMediator = mediator
|
||||||
|
)
|
||||||
|
|
||||||
|
val artistsPaged = pager
|
||||||
|
.flow
|
||||||
|
.map { artists -> artists.map { Artist.fromDecoratedEntity(it) } }
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
.asLiveData()
|
||||||
|
|
||||||
class ArtistsViewModel(private val repository: ArtistsRepository) : ViewModel() {
|
|
||||||
val artists: LiveData<List<Artist>> = repository.all().map { artists ->
|
val artists: LiveData<List<Artist>> = repository.all().map { artists ->
|
||||||
artists.map { Artist.fromDecoratedEntity(it) }
|
artists.map { Artist.fromDecoratedEntity(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.github.apognu.otter.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.github.apognu.otter.repositories.AlbumsSearchRepository
|
||||||
|
import com.github.apognu.otter.repositories.ArtistsSearchRepository
|
||||||
|
import com.github.apognu.otter.repositories.TracksSearchRepository
|
||||||
|
|
||||||
|
class SearchViewModel(private val artistsRepository: ArtistsSearchRepository, private val albumsRepository: AlbumsSearchRepository, private val tracksRepository: TracksSearchRepository) : ViewModel() {
|
||||||
|
val artists = artistsRepository.results
|
||||||
|
val albums = albumsRepository.results
|
||||||
|
val tracks = tracksRepository.results
|
||||||
|
|
||||||
|
fun search(term: String) {
|
||||||
|
artistsRepository.search(term)
|
||||||
|
albumsRepository.search(term)
|
||||||
|
tracksRepository.search(term)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue