Update.
This commit is contained in:
parent
945f227ace
commit
1126d47a1a
|
@ -2,12 +2,14 @@ package com.github.apognu.otter
|
|||
|
||||
import android.app.Application
|
||||
import android.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()) }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()) }
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.github.apognu.otter.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.apognu.otter.repositories.AlbumsSearchRepository
|
||||
import com.github.apognu.otter.repositories.ArtistsSearchRepository
|
||||
import com.github.apognu.otter.repositories.TracksSearchRepository
|
||||
|
||||
class SearchViewModel(private val artistsRepository: ArtistsSearchRepository, private val albumsRepository: AlbumsSearchRepository, private val tracksRepository: TracksSearchRepository) : ViewModel() {
|
||||
val artists = artistsRepository.results
|
||||
val albums = albumsRepository.results
|
||||
val tracks = tracksRepository.results
|
||||
|
||||
fun search(term: String) {
|
||||
artistsRepository.search(term)
|
||||
albumsRepository.search(term)
|
||||
tracksRepository.search(term)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue