diff --git a/app/src/main/java/com/github/apognu/otter/Otter.kt b/app/src/main/java/com/github/apognu/otter/Otter.kt index a3c6cbd..c14a6e6 100644 --- a/app/src/main/java/com/github/apognu/otter/Otter.kt +++ b/app/src/main/java/com/github/apognu/otter/Otter.kt @@ -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()) } }) } diff --git a/app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt index f07b938..1b26286 100644 --- a/app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt +++ b/app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt @@ -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() + private val favoritesRepository by inject() + 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) diff --git a/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt index 086b441..d4965ba 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/ArtistsAdapter.kt @@ -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() { +val DIFF = object : DiffUtil.ItemCallback() { + 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(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) } } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt index b1f0b8c..acd919d 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/PlaylistsAdapter.kt @@ -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) } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/SearchAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/SearchAdapter.kt index 2be1832..0e87d8f 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/SearchAdapter.kt @@ -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 = mutableListOf() - var albums: MutableList = mutableListOf() - var tracks: MutableList = mutableListOf() - - var currentTrack: FunkwhaleTrack? = null + var artists: MutableSet = mutableSetOf() + var albums: MutableSet = mutableSetOf() + var tracks: MutableSet = 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") diff --git a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt index 39ec1ac..784fa8d 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt @@ -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() { +class AlbumsFragment : OtterFragment() { override val repository by inject { parametersOf(null) } override val adapter by inject { parametersOf(context, OnAlbumClickListener()) } override val viewModel by viewModel { parametersOf(artistId) } diff --git a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsGridFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsGridFragment.kt index 7f6629a..b3d76ac 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/AlbumsGridFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/AlbumsGridFragment.kt @@ -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() { +class AlbumsGridFragment : OtterFragment() { override val repository by inject { parametersOf(null) } override val adapter by inject { parametersOf(context, OnAlbumClickListener()) } override val viewModel by viewModel { parametersOf(null) } @@ -30,6 +30,8 @@ class AlbumsGridFragment : LiveOtterFragment diff --git a/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt index 980a998..fef5ad1 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt @@ -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() { +class ArtistsFragment : PagedOtterFragment() { override val repository by inject() override val adapter by inject { parametersOf(context, OnArtistClickListener()) } override val viewModel by viewModel() - override val liveData by lazy { viewModel.artists } + override val 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 - 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) diff --git a/app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt index 07a7847..3cdd1df 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt @@ -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() { +class FavoritesFragment : OtterFragment() { override val repository by inject() override val adapter by inject { parametersOf(context, FavoriteListener()) } override val viewModel by viewModel() diff --git a/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt similarity index 62% rename from app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt rename to app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt index 0bb3245..1b710d8 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/LiveOtterFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/OtterFragment.kt @@ -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 : RecyclerView.Adapter() { var data: MutableList = mutableListOf() @@ -35,10 +37,8 @@ abstract class OtterAdapter : RecyclerView.Adap abstract override fun getItemId(position: Int): Long } -abstract class LiveOtterFragment> : Fragment() { - companion object { - const val OFFSCREEN_PAGES = 20 - } +abstract class OtterFragment> : Fragment() { + open val OFFSCREEN_PAGES = 10 abstract val repository: Repository abstract val adapter: A @@ -152,3 +152,82 @@ abstract class LiveOtterFragment> : F return false } } + +abstract class PagedOtterFragment> : Fragment() { + open val OFFSCREEN_PAGES = 10 + + abstract val repository: Repository + abstract val adapter: A + open val viewModel: ViewModel? = null + + abstract val liveData: LiveData> + abstract val viewRes: Int + abstract val recycler: RecyclerView + + open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context) + open val alwaysRefresh = true + + 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) {} + + 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 + } +} diff --git a/app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt index 66fb32b..1f8ceb7 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt @@ -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() { +class PlaylistTracksFragment : OtterFragment() { private val favoritesRepository by inject() override val repository by inject { parametersOf(playlistId) } override val adapter by inject { parametersOf(context, FavoriteListener()) } diff --git a/app/src/main/java/com/github/apognu/otter/fragments/PlaylistsFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/PlaylistsFragment.kt index 3bcf07e..0f143e4 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/PlaylistsFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/PlaylistsFragment.kt @@ -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() { +class PlaylistsFragment : OtterFragment() { override val repository by inject() override val adapter by inject { parametersOf(context, OnPlaylistClickListener()) } override val viewModel by viewModel() diff --git a/app/src/main/java/com/github/apognu/otter/fragments/RadiosFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/RadiosFragment.kt index 82bbf77..03cc763 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/RadiosFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/RadiosFragment.kt @@ -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() { +class RadiosFragment : OtterFragment() { override val repository by inject() override val adapter by inject { parametersOf(context, lifecycleScope, RadioClickListener()) } override val viewModel by inject() diff --git a/app/src/main/java/com/github/apognu/otter/fragments/TracksFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/TracksFragment.kt index 92a3d2d..0e5226a 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/TracksFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/TracksFragment.kt @@ -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() { +class TracksFragment : OtterFragment() { + private val favoritesRepository by inject() + override val repository by inject { parametersOf(albumId) } override val adapter by inject { parametersOf(context, FavoriteListener()) } override val viewModel by viewModel { parametersOf(albumId) } @@ -41,8 +37,6 @@ class TracksFragment : LiveOtterFragment() override val viewRes = R.layout.fragment_tracks override val recycler: RecyclerView get() = tracks - private val favoritesRepository by inject() - private var albumId = 0 companion object { @@ -59,8 +53,6 @@ class TracksFragment : LiveOtterFragment() arguments?.apply { albumId = getInt("albumId") } - - watchEventBus() } override fun onResume() { @@ -149,42 +141,6 @@ class TracksFragment : LiveOtterFragment() } } - 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) { diff --git a/app/src/main/java/com/github/apognu/otter/models/Mediator.kt b/app/src/main/java/com/github/apognu/otter/models/Mediator.kt new file mode 100644 index 0000000..237c5c1 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/models/Mediator.kt @@ -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(), KoinComponent { + override suspend fun load(loadType: LoadType, state: PagingState): 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/models/api/Album.kt b/app/src/main/java/com/github/apognu/otter/models/api/Album.kt index c7a2cbb..76227dc 100644 --- a/app/src/main/java/com/github/apognu/otter/models/api/Album.kt +++ b/app/src/main/java/com/github/apognu/otter/models/api/Album.kt @@ -21,4 +21,5 @@ data class FunkwhaleAlbum( data class Covers(val urls: CoverUrls?) @Serializable -data class CoverUrls(val original: String?) \ No newline at end of file +data class CoverUrls(val original: String?) + diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Album.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Album.kt index 667fdf6..8d135eb 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Album.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Album.kt @@ -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> - @Query("SELECT * FROM DecoratedAlbumEntity ORDER BY release_date") + @Query("SELECT * FROM DecoratedAlbumEntity ORDER BY title") fun allSync(): List - @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): LiveData> @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> @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt index 905c407..a1ff94e 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Artist.kt @@ -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 + @Query("SELECT * FROM DecoratedArtistEntity") fun allDecorated(): LiveData> @@ -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): LiveData> + @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, diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Playlist.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Playlist.kt index dfa60ad..a42eac9 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Playlist.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Playlist.kt @@ -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, val tracks_count: Int, diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Radio.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Radio.kt index 44037c8..f57dabe 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Radio.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Radio.kt @@ -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 diff --git a/app/src/main/java/com/github/apognu/otter/models/dao/Track.kt b/app/src/main/java/com/github/apognu/otter/models/dao/Track.kt index 10fd465..3584a6e 100644 --- a/app/src/main/java/com/github/apognu/otter/models/dao/Track.kt +++ b/app/src/main/java/com/github/apognu/otter/models/dao/Track.kt @@ -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? diff --git a/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt index b0ad13b..f341509 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt @@ -25,13 +25,15 @@ class AlbumsRepository(override val context: Context, private val database: Otte override fun onDataFetched(data: List): List { 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) = database.albums().findAllDecorated(ids) fun ofArtist(id: Int): LiveData> { scope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt index 42a2efe..6ceb17a 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository() { override val upstream = - HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer()) + HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=id", FunkwhaleArtist.serializer()) override fun onDataFetched(data: List): List { 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> { 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) = database.artists().findDecorated(ids) } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt b/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt index 4d7b304..638edb4 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt @@ -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(val behavior: Behavior, private val url: String, pri Single, AtOnce, Progressive } - override fun fetch(size: Int): Flow> = channelFlow { - if (behavior == Behavior.Single && size != 0) return@channelFlow + override fun fetch(size: Int): Flow> = 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(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)) } } ) diff --git a/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt index d21fc32..efd3cc5 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt @@ -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() { - override val upstream: Upstream - 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() { + override val upstream: Upstream + get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer()) - override fun onDataFetched(data: List): List = runBlocking { - /* val downloaded = TracksRepository.getDownloadedIds() ?: listOf() + private val ids: MutableList = mutableListOf() - data.map { track -> - track.favorite = favorites.contains(track.id) - track.downloaded = downloaded.contains(track.id) + private val _ids: MutableLiveData> = MutableLiveData() - track.bestUpload()?.let { upload -> - val url = mustNormalizeUrl(upload.listen_url) + val results: LiveData> = 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): List { + 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() { - override val upstream: Upstream - get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", FunkwhaleArtist.serializer()) -} - -class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository() { +class AlbumsSearchRepository(override val context: Context?, private val repository: AlbumsRepository, var query: String = "") : Repository() { override val upstream: Upstream get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", FunkwhaleAlbum.serializer()) + + private val ids: MutableList = mutableListOf() + private val _ids: MutableLiveData> = MutableLiveData() + + val results: LiveData> = Transformations.switchMap(_ids) { + repository.find(it).map { albums -> albums.map { album -> Album.fromDecoratedEntity(album) } } + } + + override fun onDataFetched(data: List): List { + 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() { + override val upstream: Upstream + get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", FunkwhaleTrack.serializer()) + + private val ids: MutableList = mutableListOf() + private val _ids: MutableLiveData> = MutableLiveData() + + val results: LiveData> = Transformations.switchMap(_ids) { + repository.find(it).map { tracks -> tracks.map { track -> Track.fromDecoratedEntity(track) } } + } + + override fun onDataFetched(data: List): List { + 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt index e2a1381..a81aeb3 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt @@ -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) = database.tracks().findAllDecorated(ids) suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id) diff --git a/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt b/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt index 4c1b6b5..2a1c32e 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt @@ -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") diff --git a/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt index c60414e..4b5a106 100644 --- a/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/ArtistsViewModel.kt @@ -1,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> = repository.all().map { artists -> artists.map { Artist.fromDecoratedEntity(it) } } diff --git a/app/src/main/java/com/github/apognu/otter/viewmodels/SearchViewModel.kt b/app/src/main/java/com/github/apognu/otter/viewmodels/SearchViewModel.kt new file mode 100644 index 0000000..146fe89 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/viewmodels/SearchViewModel.kt @@ -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) + } +} \ No newline at end of file