This commit is contained in:
Antoine POPINEAU 2020-08-03 15:22:52 +02:00
parent 945f227ace
commit 1126d47a1a
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
29 changed files with 432 additions and 277 deletions

View File

@ -2,12 +2,14 @@ package com.github.apognu.otter
import android.app.Application
import android.content.Context
import android.widget.SearchView
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.Mediator
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.playback.MediaSession
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 { LandscapeQueueFragment() }
@ -115,7 +114,7 @@ class Otter : Application() {
single { ArtistsRepository(get(), get()) }
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
viewModel { ArtistsViewModel(get()) }
viewModel { ArtistsViewModel(get(), get()) }
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
@ -145,6 +144,13 @@ class Otter : Application() {
single { (scope: CoroutineScope) -> QueueRepository(get(), scope) }
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()) }
})
}

View File

@ -4,40 +4,30 @@ import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.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.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 com.github.apognu.otter.viewmodels.SearchViewModel
import kotlinx.android.synthetic.main.activity_search.*
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 org.koin.androidx.viewmodel.ext.android.viewModel
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
lateinit var artistsRepository: ArtistsSearchRepository
lateinit var albumsRepository: AlbumsSearchRepository
lateinit var tracksRepository: TracksSearchRepository
var done = 0
override fun onCreate(savedInstanceState: Bundle?) {
@ -51,23 +41,41 @@ class SearchActivity(private val database: OtterDatabase, private val favoritesR
}
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() {
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 {
override fun onQueryTextSubmit(rawQuery: String?): Boolean {
search.clearFocus()
@ -75,68 +83,18 @@ class SearchActivity(private val database: OtterDatabase, private val favoritesR
rawQuery?.let {
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.albums.clear()
adapter.tracks.clear()
adapter.notifyDataSetChanged()
artistsRepository.fetch().untilNetwork(lifecycleScope, IO) { artists, _, _ ->
done++
val query = URLEncoder.encode(it, "UTF-8")
artists.forEach {
database.artists().run {
insert(it.toDao())
viewModel.search(query)
adapter.artists.add(Artist.fromDecoratedEntity(getDecoratedBlocking(it.id)))
}
}
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()
}
}
search_spinner.visibility = View.VISIBLE
search_empty.visibility = View.GONE
search_no_results.visibility = View.GONE
}
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 {
override fun onArtistClick(holder: View?, artist: Artist) {
ArtistsFragment.openAlbums(this@SearchActivity, artist)

View File

@ -4,24 +4,30 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.utils.maybeLoad
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.github.apognu.otter.models.domain.Artist
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_artist.view.*
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<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 {
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 {
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) {
val artist = data[position]
val artist = getItem(position)!!
Picasso.get()
.maybeLoad(maybeNormalizeUrl(artist.album_cover))
@ -50,7 +56,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
val albums = view.albums
override fun onClick(view: View?) {
data[layoutPosition].let { artist ->
getItem(layoutPosition)?.let { artist ->
listener.onClick(view, artist)
}
}

View File

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.models.dao.PlaylistEntity
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.github.apognu.otter.utils.toDurationString
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
@ -54,7 +55,7 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
}
Picasso.get()
.load(url)
.load(maybeNormalizeUrl(url))
.transform(RoundedCornersTransformation(32, 0, corner))
.into(imageView)
}

View File

@ -13,7 +13,6 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.models.domain.Artist
@ -41,11 +40,9 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
val SECTION_COUNT = 3
var artists: MutableList<Artist> = mutableListOf()
var albums: MutableList<Album> = mutableListOf()
var tracks: MutableList<Track> = mutableListOf()
var currentTrack: FunkwhaleTrack? = null
var artists: MutableSet<Artist> = mutableSetOf()
var albums: MutableSet<Album> = mutableSetOf()
var tracks: MutableSet<Track> = mutableSetOf()
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
}
ResultType.Artist.ordinal -> artists[position].id.toLong()
ResultType.Artist.ordinal -> albums[position - artists.size - 2].id.toLong()
ResultType.Track.ordinal -> tracks[position - artists.size - albums.size - SECTION_COUNT].id.toLong()
ResultType.Artist.ordinal -> artists.elementAt(position).id.toLong()
ResultType.Artist.ordinal -> albums.elementAt(position - artists.size - 2).id.toLong()
ResultType.Track.ordinal -> tracks.elementAt(position - artists.size - albums.size - SECTION_COUNT).id.toLong()
else -> 0
}
}
@ -134,19 +131,19 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
holder.actions.visibility = View.GONE
holder.favorite.visibility = View.GONE
artists[position - 1]
artists.elementAt(position - 1)
}
ResultType.Album.ordinal -> {
holder.actions.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()
@ -173,10 +170,11 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
if (resultType == ResultType.Track.ordinal) {
(item as? Track)?.let { track ->
context?.let { context ->
/* if (track == currentTrack || track.current) {
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
} */
holder.itemView.background = context.getDrawable(R.drawable.ripple)
if (track.current) {
holder.itemView.background = context.getDrawable(R.drawable.current)
}
when (track.favorite) {
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 -> {
val position = layoutPosition - 1
listener?.onArtistClick(view, artists[position])
listener?.onArtistClick(view, artists.elementAt(position))
}
ResultType.Album.ordinal -> {
val position = layoutPosition - artists.size - 2
listener?.onAlbumClick(view, albums[position])
listener?.onAlbumClick(view, albums.elementAt(position))
}
ResultType.Track.ordinal -> {
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))
context.toast("All tracks were added to your queue")

View File

@ -32,7 +32,7 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class AlbumsFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
class AlbumsFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
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) }

View File

@ -19,7 +19,7 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
class AlbumsGridFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsGridAdapter>() {
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) }
@ -30,6 +30,8 @@ class AlbumsGridFragment : LiveOtterFragment<FunkwhaleAlbum, Album, AlbumsGridAd
override val layoutManager get() = GridLayoutManager(context, 3)
override val alwaysRefresh = false
override val OFFSCREEN_PAGES = 5
inner class OnAlbumClickListener : AlbumsGridAdapter.OnAlbumClickListener {
override fun onClick(view: View?, album: Album) {
(context as? MainActivity)?.let { activity ->

View File

@ -28,12 +28,12 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class ArtistsFragment : LiveOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
class ArtistsFragment : PagedOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapter>() {
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 liveData by lazy { viewModel.artistsPaged }
override val viewRes = R.layout.fragment_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)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
artists.layoutManager = LinearLayoutManager(context)
(artists.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
artists.adapter = adapter
liveData.observe(viewLifecycleOwner) { result ->
adapter.data.size.let { position ->
adapter.data = result.toMutableList()
adapter.notifyItemInserted(position)
}
}
}
inner class OnArtistClickListener : ArtistsAdapter.OnArtistClickListener {
override fun onClick(holder: View?, artist: Artist) {
openAlbums(context, artist, this@ArtistsFragment, artist.album_cover)

View File

@ -23,7 +23,7 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class FavoritesFragment : LiveOtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
class FavoritesFragment : OtterFragment<FunkwhaleTrack, Track, FavoritesAdapter>() {
override val repository by inject<FavoritesRepository>()
override val adapter by inject<FavoritesAdapter> { parametersOf(context, FavoriteListener()) }
override val viewModel by viewModel<FavoritesViewModel>()

View File

@ -9,6 +9,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.utils.Event
import com.github.apognu.otter.utils.EventBus
import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.untilNetwork
import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.ext.scope
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
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 class LiveOtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragment() {
companion object {
const val OFFSCREEN_PAGES = 20
}
abstract class OtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragment() {
open val OFFSCREEN_PAGES = 10
abstract val repository: Repository<DAO>
abstract val adapter: A
@ -152,3 +152,82 @@ abstract class LiveOtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : F
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
}
}

View File

@ -25,7 +25,7 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class PlaylistTracksFragment : LiveOtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
class PlaylistTracksFragment : OtterFragment<FunkwhalePlaylistTrack, Track, PlaylistTracksAdapter>() {
private val favoritesRepository by inject<FavoritesRepository>()
override val repository by inject<PlaylistTracksRepository> { parametersOf(playlistId) }
override val adapter by inject<PlaylistTracksAdapter> { parametersOf(context, FavoriteListener()) }

View File

@ -19,7 +19,7 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class PlaylistsFragment : LiveOtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
class PlaylistsFragment : OtterFragment<FunkwhalePlaylist, PlaylistEntity, PlaylistsAdapter>() {
override val repository by inject<PlaylistsRepository>()
override val adapter by inject<PlaylistsAdapter> { parametersOf(context, OnPlaylistClickListener()) }
override val viewModel by viewModel<PlaylistsViewModel>()

View File

@ -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.dao.RadioEntity
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 kotlinx.android.synthetic.main.fragment_radios.*
import kotlinx.coroutines.Dispatchers.Main
@ -17,7 +20,7 @@ import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
class RadiosFragment : LiveOtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
class RadiosFragment : OtterFragment<FunkwhaleRadio, RadioEntity, RadiosAdapter>() {
override val repository by inject<RadiosRepository>()
override val adapter by inject<RadiosAdapter> { parametersOf(context, lifecycleScope, RadioClickListener()) }
override val viewModel by inject<RadiosViewModel>()

View File

@ -3,36 +3,32 @@ package com.github.apognu.otter.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.TracksAdapter
import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.domain.Album
import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.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.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
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.androidx.viewmodel.ext.android.viewModel
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 adapter by inject<TracksAdapter> { parametersOf(context, FavoriteListener()) }
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 recycler: RecyclerView get() = tracks
private val favoritesRepository by inject<FavoritesRepository>()
private var albumId = 0
companion object {
@ -59,8 +53,6 @@ class TracksFragment : LiveOtterFragment<FunkwhaleTrack, Track, TracksAdapter>()
arguments?.apply {
albumId = getInt("albumId")
}
watchEventBus()
}
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 {
override fun onToggleFavorite(id: Int, state: Boolean) {
when (state) {

View File

@ -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)
}
}
}

View File

@ -21,4 +21,5 @@ data class FunkwhaleAlbum(
data class Covers(val urls: CoverUrls?)
@Serializable
data class CoverUrls(val original: String?)
data class CoverUrls(val original: String?)

View File

@ -9,6 +9,7 @@ import com.github.apognu.otter.models.api.FunkwhaleArtist
data class AlbumEntity(
@PrimaryKey
val id: Int,
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
val title: String,
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = ForeignKey.CASCADE)
val artist_id: Int,
@ -18,13 +19,13 @@ data class AlbumEntity(
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM DecoratedAlbumEntity")
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY title")
fun allDecorated(): LiveData<List<DecoratedAlbumEntity>>
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY release_date")
@Query("SELECT * FROM DecoratedAlbumEntity ORDER BY title")
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>>
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
@ -33,7 +34,7 @@ data class AlbumEntity(
@Query("SELECT * FROM DecoratedAlbumEntity WHERE id == :id")
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>>
@Insert(onConflict = OnConflictStrategy.REPLACE)

View File

@ -1,6 +1,7 @@
package com.github.apognu.otter.models.dao
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.*
import com.github.apognu.otter.models.api.FunkwhaleArtist
import io.realm.RealmObject
@ -10,12 +11,15 @@ import io.realm.annotations.Required
data class ArtistEntity(
@PrimaryKey
val id: Int,
@ColumnInfo(collate = ColumnInfo.LOCALIZED, index = true)
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
val name: String
) {
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM DecoratedArtistEntity")
fun allPaged(): DataSource.Factory<Int, DecoratedArtistEntity>
@Query("SELECT * FROM DecoratedArtistEntity")
fun allDecorated(): LiveData<List<DecoratedArtistEntity>>
@ -25,6 +29,9 @@ data class ArtistEntity(
@Query("SELECT * FROM DecoratedArtistEntity WHERE id == :id")
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)
fun insert(artist: ArtistEntity)
@ -34,6 +41,7 @@ data class ArtistEntity(
}
fun FunkwhaleArtist.toDao() = run {
ArtistEntity(id, name)
}
@ -43,7 +51,7 @@ fun FunkwhaleArtist.toDao() = run {
INNER JOIN albums
ON albums.artist_id = artists.id
GROUP BY albums.artist_id
ORDER BY name
ORDER BY artists.id
""")
data class DecoratedArtistEntity(
val id: Int,

View File

@ -8,6 +8,7 @@ import com.github.apognu.otter.models.api.FunkwhalePlaylist
data class PlaylistEntity(
@PrimaryKey
val id: Int,
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
val name: String,
val album_covers: List<String>,
val tracks_count: Int,

View File

@ -9,6 +9,7 @@ data class RadioEntity(
@PrimaryKey
val id: Int,
var radio_type: String?,
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
val name: String,
val description: String,
var related_object_id: String? = null

View File

@ -3,19 +3,19 @@ package com.github.apognu.otter.models.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import androidx.room.ForeignKey.CASCADE
import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack
import org.koin.java.KoinJavaComponent.inject
@Entity(tableName = "tracks")
data class TrackEntity(
@PrimaryKey
val id: Int,
@ColumnInfo(collate = ColumnInfo.UNICODE, index = true)
val title: String,
@ForeignKey(entity = ArtistEntity::class, parentColumns = ["id"], childColumns = ["artist_id"], onDelete = CASCADE)
val artist_id: Int,
@ForeignKey(entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["album_id"], onDelete = CASCADE)
val album_id: Int?,
@ColumnInfo(index = true)
val position: Int?,
val copyright: String?,
val license: String?

View File

@ -25,13 +25,15 @@ class AlbumsRepository(override val context: Context, private val database: Otte
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach {
database.albums().insert(it.toDao())
insert(it)
}
return super.onDataFetched(data)
}
fun insert(album: FunkwhaleAlbum) = database.albums().insert(album.toDao())
fun all() = database.albums().allDecorated()
fun find(ids: List<Int>) = database.albums().findAllDecorated(ids)
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> {
scope.launch(Dispatchers.IO) {

View File

@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
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())
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=id", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
scope.launch(IO) {
@ -34,6 +34,10 @@ class ArtistsRepository(override val context: Context, private val database: Ott
return super.onDataFetched(data)
}
fun insert(artist: FunkwhaleArtist) = database.artists().insert(artist.toDao())
fun allPaged() = database.artists().allPaged()
fun all(): LiveData<List<DecoratedArtistEntity>> {
scope.launch(IO) {
fetch().collect()
@ -41,5 +45,7 @@ class ArtistsRepository(override val context: Context, private val database: Ott
return database.artists().allDecorated()
}
fun get(id: Int) = database.artists().getDecorated(id)
fun find(ids: List<Int>) = database.artists().findDecorated(ids)
}

View File

@ -10,10 +10,7 @@ import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.github.kittinunf.result.Result
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.*
import kotlinx.serialization.KSerializer
import kotlin.math.ceil
@ -22,8 +19,8 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
Single, AtOnce, Progressive
}
override fun fetch(size: Int): Flow<Repository.Response<D>> = channelFlow {
if (behavior == Behavior.Single && size != 0) return@channelFlow
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
if (behavior == Behavior.Single && size != 0) return@flow
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
when (behavior) {
Behavior.Single -> send(Repository.Response(data, page, false))
Behavior.Progressive -> send(Repository.Response(data, page, response.next != null))
Behavior.Single -> emit(Repository.Response(data, page, false))
Behavior.Progressive -> emit(Repository.Response(data, page, response.next != null))
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.log()
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> send(Repository.Response(listOf(), page, false))
else -> emit(Repository.Response(listOf(), page, false))
}
}
)

View File

@ -1,41 +1,117 @@
package com.github.apognu.otter.repositories
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.FunkwhaleArtist
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>() {
override val upstream: Upstream<FunkwhaleTrack>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer())
class ArtistsSearchRepository(override val context: Context?, private val repository: ArtistsRepository, var query: String = "") : Repository<FunkwhaleArtist>() {
override val upstream: Upstream<FunkwhaleArtist>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
/* val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
private val ids: MutableList<Int> = mutableListOf()
data.map { track ->
track.favorite = favorites.contains(track.id)
track.downloaded = downloaded.contains(track.id)
private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
track.bestUpload()?.let { upload ->
val url = mustNormalizeUrl(upload.listen_url)
val results: LiveData<List<Artist>> = Transformations.switchMap(_ids) {
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>() {
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>() {
class AlbumsSearchRepository(override val context: Context?, private val repository: AlbumsRepository, var query: String = "") : Repository<FunkwhaleAlbum>() {
override val upstream: Upstream<FunkwhaleAlbum>
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()
}
}
}

View File

@ -46,6 +46,10 @@ class TracksRepository(override val context: Context, private val database: Otte
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)
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)

View File

@ -39,24 +39,6 @@ object AppContext {
fun init(context: Activity) {
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")

View File

@ -1,12 +1,29 @@
package com.github.apognu.otter.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.*
import androidx.paging.Pager
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.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 ->
artists.map { Artist.fromDecoratedEntity(it) }
}

View File

@ -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)
}
}