Put buggy features behind an experiments gate (favorites, for now). Optimized layouts to be able to load lots of content. Fixed Funkwhale API URLs to try and be backward compatible.
This commit is contained in:
parent
a63f3f7e68
commit
7c9a71d6d7
|
@ -97,7 +97,7 @@ dependencies {
|
|||
implementation("androidx.preference:preference:1.1.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.0.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
implementation("com.google.android.material:material:1.1.0-beta01")
|
||||
implementation("com.google.android.material:material:1.2.0-alpha01")
|
||||
implementation("com.android.support.constraint:constraint-layout:1.1.3")
|
||||
|
||||
implementation("com.google.android.exoplayer:exoplayer:2.10.5")
|
||||
|
|
|
@ -64,7 +64,7 @@ class LoginActivity : AppCompatActivity() {
|
|||
|
||||
GlobalScope.launch(Main) {
|
||||
try {
|
||||
val result = Fuel.post("$hostname/api/v1/token", body)
|
||||
val result = Fuel.post("$hostname/api/v1/token/", body)
|
||||
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
|
||||
result.fold(
|
||||
|
|
|
@ -256,7 +256,7 @@ class MainActivity : AppCompatActivity() {
|
|||
.centerCrop()
|
||||
.into(now_playing_details_cover)
|
||||
|
||||
favoriteCheckRepository.fetch().untilNetwork(IO) { favorites ->
|
||||
favoriteCheckRepository.fetch().untilNetwork(IO) { favorites, _ ->
|
||||
GlobalScope.launch(Main) {
|
||||
track.favorite = favorites.contains(track.id)
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class SearchActivity : AppCompatActivity() {
|
|||
adapter.data.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
repository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks ->
|
||||
repository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks, _ ->
|
||||
search_spinner.visibility = View.GONE
|
||||
search_empty.visibility = View.GONE
|
||||
|
||||
|
|
|
@ -48,6 +48,17 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
|||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
when (preference?.key) {
|
||||
"oss_licences" -> startActivity(Intent(activity, LicencesActivity::class.java))
|
||||
|
||||
"experiments" -> {
|
||||
context?.let { context ->
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.settings_experiments_restart_title))
|
||||
.setMessage(context.getString(R.string.settings_experiments_restart_content))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
"logout" -> {
|
||||
context?.let { context ->
|
||||
AlertDialog.Builder(context)
|
||||
|
|
|
@ -8,11 +8,17 @@ import com.github.apognu.otter.fragments.AlbumsGridFragment
|
|||
import com.github.apognu.otter.fragments.ArtistsFragment
|
||||
import com.github.apognu.otter.fragments.FavoritesFragment
|
||||
import com.github.apognu.otter.fragments.PlaylistsFragment
|
||||
import com.preference.PowerPreference
|
||||
|
||||
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
var tabs = mutableListOf<Fragment>()
|
||||
|
||||
override fun getCount() = 4
|
||||
override fun getCount(): Int {
|
||||
return when (PowerPreference.getDefaultFile().getBoolean("experiments", false)) {
|
||||
true -> 4
|
||||
false -> 3
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
tabs.getOrNull(position)?.let {
|
||||
|
|
|
@ -29,7 +29,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
arguments = bundleOf(
|
||||
"artistId" to artist.id,
|
||||
"artistName" to artist.name,
|
||||
"artistArt" to artist.albums!![0].cover.original
|
||||
"artistArt" to if (artist.albums?.isNotEmpty() == true) artist.albums[0].cover.original else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@ class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
|
|||
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
|
||||
override var fetchOnCreate = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
|
@ -4,13 +4,19 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.repositories.HttpUpstream
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.untilNetwork
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
var data: MutableList<D> = mutableListOf()
|
||||
|
@ -24,7 +30,6 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
lateinit var repository: Repository<D, *>
|
||||
lateinit var adapter: A
|
||||
|
||||
open var fetchOnCreate = true
|
||||
private var initialFetched = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -37,56 +42,64 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
recycler.layoutManager = layoutManager
|
||||
recycler.adapter = adapter
|
||||
|
||||
scroller?.setOnScrollChangeListener { _: NestedScrollView?, _: Int, _: Int, _: Int, _: Int ->
|
||||
if (!scroller.canScrollVertically(1)) {
|
||||
repository.fetch(Repository.Origin.Network.origin, adapter.data).untilNetwork {
|
||||
swiper?.isRefreshing = false
|
||||
|
||||
onDataFetched(it)
|
||||
|
||||
adapter.data = it.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!recycler.canScrollVertically(1)) {
|
||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swiper?.isRefreshing = true
|
||||
|
||||
if (fetchOnCreate) fetch()
|
||||
fetch()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
recycler.adapter = adapter
|
||||
|
||||
swiper?.setOnRefreshListener {
|
||||
repository.fetch(Repository.Origin.Network.origin, listOf()).untilNetwork {
|
||||
swiper?.isRefreshing = false
|
||||
|
||||
onDataFetched(it)
|
||||
|
||||
adapter.data = it.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
if (!fetchOnCreate) fetch()
|
||||
}
|
||||
|
||||
open fun onDataFetched(data: List<D>) {}
|
||||
|
||||
private fun fetch() {
|
||||
if (!initialFetched) {
|
||||
initialFetched = true
|
||||
private fun fetch(upstreams: Int = (Repository.Origin.Network.origin and Repository.Origin.Cache.origin), size: Int = 0) {
|
||||
var cleared = false
|
||||
|
||||
repository.fetch().untilNetwork {
|
||||
swiper?.isRefreshing = true
|
||||
|
||||
if (size == 0) {
|
||||
cleared = true
|
||||
adapter.data.clear()
|
||||
}
|
||||
|
||||
repository.fetch(upstreams, size).untilNetwork(IO) { data, hasMore ->
|
||||
onDataFetched(data)
|
||||
|
||||
if (!hasMore) {
|
||||
swiper?.isRefreshing = false
|
||||
|
||||
onDataFetched(it)
|
||||
repository.cacheId?.let { cacheId ->
|
||||
Cache.set(
|
||||
context,
|
||||
cacheId,
|
||||
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.data = it.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
GlobalScope.launch(Main) {
|
||||
adapter.data.addAll(data)
|
||||
|
||||
when (cleared) {
|
||||
true -> {
|
||||
adapter.notifyDataSetChanged()
|
||||
cleared = false
|
||||
}
|
||||
false -> adapter.notifyItemRangeInserted(adapter.data.size, data.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,9 +99,11 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
|
|||
else -> cover_top_left
|
||||
}
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(url))
|
||||
.into(imageView)
|
||||
GlobalScope.launch(Main) {
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(url))
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -403,7 +403,6 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
override fun onPlayerError(error: ExoPlaybackException?) {
|
||||
log(error.toString())
|
||||
EventBus.send(
|
||||
Event.PlaybackError(
|
||||
getString(R.string.error_playback)
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.github.apognu.otter.playback
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||
|
@ -94,8 +93,6 @@ class QueueManager(val context: Context) {
|
|||
val sources = tracks.map { track ->
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
log(url)
|
||||
|
||||
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url))
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
|||
|
||||
override val upstream: Upstream<Album> by lazy {
|
||||
val url =
|
||||
if (artistId == null) "/api/v1/albums?playable=true"
|
||||
else "/api/v1/albums?playable=true&artist=$artistId"
|
||||
if (artistId == null) "/api/v1/albums/?playable=true"
|
||||
else "/api/v1/albums/?playable=true&artist=$artistId"
|
||||
|
||||
HttpUpstream<Album, FunkwhaleResponse<Album>>(
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId = "artists"
|
||||
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists?playable=true", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -14,7 +14,7 @@ import java.io.BufferedReader
|
|||
|
||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "favorites.v2"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks?favorites=true&playable=true", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
@ -30,7 +30,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
|
||||
runBlocking(IO) {
|
||||
Fuel
|
||||
.post(mustNormalizeUrl("/api/v1/favorites/tracks"))
|
||||
.post(mustNormalizeUrl("/api/v1/favorites/tracks/"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
|
@ -55,7 +55,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||
override val cacheId = "favorited"
|
||||
override val upstream = HttpUpstream<Int, FunkwhaleResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Int, FunkwhaleResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -18,7 +18,7 @@ import java.io.Reader
|
|||
import java.lang.reflect.Type
|
||||
import kotlin.math.ceil
|
||||
|
||||
class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
enum class Behavior {
|
||||
Single, AtOnce, Progressive
|
||||
}
|
||||
|
@ -33,10 +33,10 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
|
|||
return _channel!!
|
||||
}
|
||||
|
||||
override fun fetch(data: List<D>): Channel<Repository.Response<D>>? {
|
||||
if (behavior == Behavior.Single && data.isNotEmpty()) return null
|
||||
override fun fetch(size: Int): Channel<Repository.Response<D>>? {
|
||||
if (behavior == Behavior.Single && size != 0) return null
|
||||
|
||||
val page = ceil(data.size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val offsetUrl =
|
||||
|
@ -49,19 +49,22 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
|
|||
|
||||
get(offsetUrl).fold(
|
||||
{ response ->
|
||||
val data = data.plus(response.getData())
|
||||
|
||||
log(data.size.toString())
|
||||
val data = response.getData()
|
||||
|
||||
if (behavior == Behavior.Progressive || response.next == null) {
|
||||
channel.offer(Repository.Response(Repository.Origin.Network, data))
|
||||
channel.offer(Repository.Response(Repository.Origin.Network, data, false))
|
||||
} else {
|
||||
fetch(data)
|
||||
channel.offer(Repository.Response(Repository.Origin.Network, data, true))
|
||||
|
||||
fetch(size + data.size)
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
log(error.toString())
|
||||
|
||||
when (error.exception) {
|
||||
is RefreshError -> EventBus.send(Event.LogOut)
|
||||
else -> channel.offer(Repository.Response(Repository.Origin.Network, listOf(), false))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -77,8 +80,6 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
|
|||
}
|
||||
|
||||
suspend fun get(url: String): Result<R, FuelError> {
|
||||
log(url)
|
||||
|
||||
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
|
||||
|
||||
val (_, response, result) = Fuel
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.io.BufferedReader
|
|||
|
||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||
override val cacheId = "tracks-playlist-$playlistId"
|
||||
override val upstream = HttpUpstream<PlaylistTrack, FunkwhaleResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<PlaylistTrack, FunkwhaleResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||
override val cacheId = "tracks-playlists"
|
||||
override val upstream = HttpUpstream<Playlist, FunkwhaleResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists?playable=true", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Playlist, FunkwhaleResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -6,11 +6,13 @@ import com.github.apognu.otter.utils.CacheItem
|
|||
import com.github.apognu.otter.utils.untilNetwork
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedReader
|
||||
|
||||
interface Upstream<D> {
|
||||
fun fetch(data: List<D> = listOf()): Channel<Repository.Response<D>>?
|
||||
fun fetch(size: Int = 0): Channel<Repository.Response<D>>?
|
||||
}
|
||||
|
||||
abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||
|
@ -19,7 +21,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
|||
Network(0b10)
|
||||
}
|
||||
|
||||
data class Response<D>(val origin: Origin, val data: List<D>)
|
||||
data class Response<D>(val origin: Origin, val data: List<D>, val hasMore: Boolean)
|
||||
|
||||
abstract val context: Context?
|
||||
abstract val cacheId: String?
|
||||
|
@ -35,29 +37,31 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
|||
return _channel!!
|
||||
}
|
||||
|
||||
protected open fun cache(data: List<D>): C? = null
|
||||
open fun cache(data: List<D>): C? = null
|
||||
protected open fun uncache(reader: BufferedReader): C? = null
|
||||
|
||||
fun fetch(upstreams: Int = Origin.Cache.origin and Origin.Network.origin, from: List<D> = listOf()): Channel<Response<D>> {
|
||||
fun fetch(upstreams: Int = Origin.Cache.origin and Origin.Network.origin, size: Int = 0): Channel<Response<D>> {
|
||||
if (Origin.Cache.origin and upstreams == upstreams) fromCache()
|
||||
if (Origin.Network.origin and upstreams == upstreams) fromNetwork(from)
|
||||
if (Origin.Network.origin and upstreams == upstreams) fromNetwork(size)
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
private fun fromCache() {
|
||||
cacheId?.let { cacheId ->
|
||||
Cache.get(context, cacheId)?.let { reader ->
|
||||
uncache(reader)?.let { cache ->
|
||||
channel.offer(Response(Origin.Cache, cache.data))
|
||||
GlobalScope.launch(IO) {
|
||||
cacheId?.let { cacheId ->
|
||||
Cache.get(context, cacheId)?.let { reader ->
|
||||
uncache(reader)?.let { cache ->
|
||||
channel.offer(Response(Origin.Cache, cache.data, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromNetwork(from: List<D>) {
|
||||
upstream.fetch(data = from)?.untilNetwork(IO) {
|
||||
val data = onDataFetched(it)
|
||||
private fun fromNetwork(size: Int) {
|
||||
upstream.fetch(size)?.untilNetwork(IO) { data, hasMore ->
|
||||
val data = onDataFetched(data)
|
||||
|
||||
cacheId?.let { cacheId ->
|
||||
Cache.set(
|
||||
|
@ -67,7 +71,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
|||
)
|
||||
}
|
||||
|
||||
channel.offer(Response(Origin.Network, data))
|
||||
channel.offer(Response(Origin.Network, data, hasMore))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.io.BufferedReader
|
|||
|
||||
class SearchRepository(override val context: Context?, query: String) : Repository<Track, TracksCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.io.BufferedReader
|
|||
|
||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-album-$albumId"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks?playable=true&album=$albumId", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -3,7 +3,10 @@ package com.github.apognu.otter.utils
|
|||
import com.github.apognu.otter.Otter
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.filter
|
||||
import kotlinx.coroutines.channels.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Command {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.github.apognu.otter.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -29,12 +28,12 @@ inline fun <D> Channel<Repository.Response<D>>.await(context: CoroutineContext =
|
|||
}
|
||||
}
|
||||
|
||||
inline fun <D> Channel<Repository.Response<D>>.untilNetwork(context: CoroutineContext = Main, crossinline callback: (data: List<D>) -> Unit) {
|
||||
inline fun <D> Channel<Repository.Response<D>>.untilNetwork(context: CoroutineContext = Main, crossinline callback: (data: List<D>, hasMore: Boolean) -> Unit) {
|
||||
GlobalScope.launch(context) {
|
||||
for (data in this@untilNetwork) {
|
||||
callback(data.data)
|
||||
callback(data.data, data.hasMore)
|
||||
|
||||
if (data.origin == Repository.Origin.Network) {
|
||||
if (data.origin == Repository.Origin.Network && !data.hasMore) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||
</vector>
|
|
@ -1,47 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroller"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true">
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/albums"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent"
|
||||
tools:itemCount="10"
|
||||
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
tools:listitem="@layout/row_album_grid"
|
||||
tools:spanCount="3" />
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/albums" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/albums"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:itemCount="10"
|
||||
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
tools:listitem="@layout/row_album_grid"
|
||||
tools:spanCount="3" />
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/albums" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,49 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroller"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true">
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/artists"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
tools:listitem="@layout/row_artist" />
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/artists" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/artists"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_artist" />
|
||||
android:orientation="vertical">
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/artists" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,27 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroller"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/favorites"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/row_track" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false">
|
||||
android:clipChildren="false"
|
||||
app:layout_collapseMode="parallax">
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
|
@ -48,13 +64,8 @@
|
|||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/favorites"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/row_track" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,49 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroller"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swiper"
|
||||
style="@style/AppTheme.Fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true">
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/playlists"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_playlist" />
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/playlists" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/playlists"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_playlist" />
|
||||
android:orientation="vertical">
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/playlists" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -38,6 +38,10 @@
|
|||
<string name="settings_night_mode_off_summary">Le mode jour sera toujours préféré</string>
|
||||
<string name="settings_night_mode_system">Suivre les préférences du système</string>
|
||||
<string name="settings_night_mode_system_summary">Le mode nuit suivra les préférence système</string>
|
||||
<string name="settings_experiments">Activer les fonctionnalité expérimentales</string>
|
||||
<string name="settings_experiments_description">Utiliser à vos risques et périls, peut potentiellement ralentir ou crasher l\'application</string>
|
||||
<string name="settings_experiments_restart_title">Relancement requis</string>
|
||||
<string name="settings_experiments_restart_content">Veuillez tuer puis relancer l\'application afin que ce changement soit pris en compte</string>
|
||||
<string name="settings_logout">Déconnexion</string>
|
||||
|
||||
<string name="artists">Artistes</string>
|
||||
|
|
|
@ -38,6 +38,10 @@
|
|||
<string name="settings_night_mode_off_summary">Light mode will always be preferred</string>
|
||||
<string name="settings_night_mode_system">Follow system settings</string>
|
||||
<string name="settings_night_mode_system_summary">Night mode will follow system settings</string>
|
||||
<string name="settings_experiments">Enable experimental features</string>
|
||||
<string name="settings_experiments_description">Use at your own risks, may freeze or crash the app</string>
|
||||
<string name="settings_experiments_restart_title">Restart required</string>
|
||||
<string name="settings_experiments_restart_content">Please kill and restart the app in order for this change to take effect</string>
|
||||
<string name="settings_logout">Sign out</string>
|
||||
|
||||
<string name="artists">Artists</string>
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
android:key="oss_licences"
|
||||
android:title="@string/title_oss_licences" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:icon="@drawable/experimental"
|
||||
android:key="experiments"
|
||||
android:title="@string/settings_experiments"
|
||||
android:summary="@string/settings_experiments_description"/>
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/logout"
|
||||
android:key="logout"
|
||||
|
|
Loading…
Reference in New Issue