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.preference:preference:1.1.0")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.0.0")
|
implementation("androidx.recyclerview:recyclerview:1.0.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout: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.android.support.constraint:constraint-layout:1.1.3")
|
||||||
|
|
||||||
implementation("com.google.android.exoplayer:exoplayer:2.10.5")
|
implementation("com.google.android.exoplayer:exoplayer:2.10.5")
|
||||||
|
|
|
@ -64,7 +64,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
GlobalScope.launch(Main) {
|
GlobalScope.launch(Main) {
|
||||||
try {
|
try {
|
||||||
val result = Fuel.post("$hostname/api/v1/token", body)
|
val result = Fuel.post("$hostname/api/v1/token/", body)
|
||||||
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
|
|
|
@ -256,7 +256,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(now_playing_details_cover)
|
.into(now_playing_details_cover)
|
||||||
|
|
||||||
favoriteCheckRepository.fetch().untilNetwork(IO) { favorites ->
|
favoriteCheckRepository.fetch().untilNetwork(IO) { favorites, _ ->
|
||||||
GlobalScope.launch(Main) {
|
GlobalScope.launch(Main) {
|
||||||
track.favorite = favorites.contains(track.id)
|
track.favorite = favorites.contains(track.id)
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class SearchActivity : AppCompatActivity() {
|
||||||
adapter.data.clear()
|
adapter.data.clear()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|
||||||
repository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks ->
|
repository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks, _ ->
|
||||||
search_spinner.visibility = View.GONE
|
search_spinner.visibility = View.GONE
|
||||||
search_empty.visibility = View.GONE
|
search_empty.visibility = View.GONE
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,17 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
||||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||||
when (preference?.key) {
|
when (preference?.key) {
|
||||||
"oss_licences" -> startActivity(Intent(activity, LicencesActivity::class.java))
|
"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" -> {
|
"logout" -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
AlertDialog.Builder(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.ArtistsFragment
|
||||||
import com.github.apognu.otter.fragments.FavoritesFragment
|
import com.github.apognu.otter.fragments.FavoritesFragment
|
||||||
import com.github.apognu.otter.fragments.PlaylistsFragment
|
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) {
|
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
var tabs = mutableListOf<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 {
|
override fun getItem(position: Int): Fragment {
|
||||||
tabs.getOrNull(position)?.let {
|
tabs.getOrNull(position)?.let {
|
||||||
|
|
|
@ -29,7 +29,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
"artistId" to artist.id,
|
"artistId" to artist.id,
|
||||||
"artistName" to artist.name,
|
"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
|
lateinit var favoritesRepository: FavoritesRepository
|
||||||
|
|
||||||
override var fetchOnCreate = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,19 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.apognu.otter.repositories.HttpUpstream
|
||||||
import com.github.apognu.otter.repositories.Repository
|
import com.github.apognu.otter.repositories.Repository
|
||||||
|
import com.github.apognu.otter.utils.Cache
|
||||||
import com.github.apognu.otter.utils.untilNetwork
|
import com.github.apognu.otter.utils.untilNetwork
|
||||||
|
import com.google.gson.Gson
|
||||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
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>() {
|
abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||||
var data: MutableList<D> = mutableListOf()
|
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 repository: Repository<D, *>
|
||||||
lateinit var adapter: A
|
lateinit var adapter: A
|
||||||
|
|
||||||
open var fetchOnCreate = true
|
|
||||||
private var initialFetched = false
|
private var initialFetched = false
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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.layoutManager = layoutManager
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
|
|
||||||
scroller?.setOnScrollChangeListener { _: NestedScrollView?, _: Int, _: Int, _: Int, _: Int ->
|
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||||
if (!scroller.canScrollVertically(1)) {
|
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||||
repository.fetch(Repository.Origin.Network.origin, adapter.data).untilNetwork {
|
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||||
swiper?.isRefreshing = false
|
if (!recycler.canScrollVertically(1)) {
|
||||||
|
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||||
onDataFetched(it)
|
}
|
||||||
|
|
||||||
adapter.data = it.toMutableList()
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swiper?.isRefreshing = true
|
fetch()
|
||||||
|
|
||||||
if (fetchOnCreate) fetch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
recycler.adapter = adapter
|
|
||||||
|
|
||||||
swiper?.setOnRefreshListener {
|
swiper?.setOnRefreshListener {
|
||||||
repository.fetch(Repository.Origin.Network.origin, listOf()).untilNetwork {
|
fetch(Repository.Origin.Network.origin)
|
||||||
swiper?.isRefreshing = false
|
|
||||||
|
|
||||||
onDataFetched(it)
|
|
||||||
|
|
||||||
adapter.data = it.toMutableList()
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchOnCreate) fetch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onDataFetched(data: List<D>) {}
|
open fun onDataFetched(data: List<D>) {}
|
||||||
|
|
||||||
private fun fetch() {
|
private fun fetch(upstreams: Int = (Repository.Origin.Network.origin and Repository.Origin.Cache.origin), size: Int = 0) {
|
||||||
if (!initialFetched) {
|
var cleared = false
|
||||||
initialFetched = true
|
|
||||||
|
|
||||||
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
|
swiper?.isRefreshing = false
|
||||||
|
|
||||||
onDataFetched(it)
|
repository.cacheId?.let { cacheId ->
|
||||||
|
Cache.set(
|
||||||
|
context,
|
||||||
|
cacheId,
|
||||||
|
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter.data = it.toMutableList()
|
GlobalScope.launch(Main) {
|
||||||
adapter.notifyDataSetChanged()
|
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
|
else -> cover_top_left
|
||||||
}
|
}
|
||||||
|
|
||||||
Picasso.get()
|
GlobalScope.launch(Main) {
|
||||||
.maybeLoad(maybeNormalizeUrl(url))
|
Picasso.get()
|
||||||
.into(imageView)
|
.maybeLoad(maybeNormalizeUrl(url))
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -403,7 +403,6 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: ExoPlaybackException?) {
|
override fun onPlayerError(error: ExoPlaybackException?) {
|
||||||
log(error.toString())
|
|
||||||
EventBus.send(
|
EventBus.send(
|
||||||
Event.PlaybackError(
|
Event.PlaybackError(
|
||||||
getString(R.string.error_playback)
|
getString(R.string.error_playback)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package com.github.apognu.otter.playback
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||||
|
@ -94,8 +93,6 @@ class QueueManager(val context: Context) {
|
||||||
val sources = tracks.map { track ->
|
val sources = tracks.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
log(url)
|
|
||||||
|
|
||||||
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(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 {
|
override val upstream: Upstream<Album> by lazy {
|
||||||
val url =
|
val url =
|
||||||
if (artistId == null) "/api/v1/albums?playable=true"
|
if (artistId == null) "/api/v1/albums/?playable=true"
|
||||||
else "/api/v1/albums?playable=true&artist=$artistId"
|
else "/api/v1/albums/?playable=true&artist=$artistId"
|
||||||
|
|
||||||
HttpUpstream<Album, FunkwhaleResponse<Album>>(
|
HttpUpstream<Album, FunkwhaleResponse<Album>>(
|
||||||
HttpUpstream.Behavior.Progressive,
|
HttpUpstream.Behavior.Progressive,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
||||||
|
|
||||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||||
override val cacheId = "artists"
|
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 cache(data: List<Artist>) = ArtistsCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
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>() {
|
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||||
override val cacheId = "favorites.v2"
|
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 cache(data: List<Track>) = TracksCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
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) {
|
runBlocking(IO) {
|
||||||
Fuel
|
Fuel
|
||||||
.post(mustNormalizeUrl("/api/v1/favorites/tracks"))
|
.post(mustNormalizeUrl("/api/v1/favorites/tracks/"))
|
||||||
.header("Authorization", "Bearer $token")
|
.header("Authorization", "Bearer $token")
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(Gson().toJson(body))
|
.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>() {
|
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||||
override val cacheId = "favorited"
|
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 cache(data: List<Int>) = FavoritedCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
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 java.lang.reflect.Type
|
||||||
import kotlin.math.ceil
|
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 {
|
enum class Behavior {
|
||||||
Single, AtOnce, Progressive
|
Single, AtOnce, Progressive
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,10 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
|
||||||
return _channel!!
|
return _channel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetch(data: List<D>): Channel<Repository.Response<D>>? {
|
override fun fetch(size: Int): Channel<Repository.Response<D>>? {
|
||||||
if (behavior == Behavior.Single && data.isNotEmpty()) return null
|
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) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val offsetUrl =
|
val offsetUrl =
|
||||||
|
@ -49,19 +49,22 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
|
||||||
|
|
||||||
get(offsetUrl).fold(
|
get(offsetUrl).fold(
|
||||||
{ response ->
|
{ response ->
|
||||||
val data = data.plus(response.getData())
|
val data = response.getData()
|
||||||
|
|
||||||
log(data.size.toString())
|
|
||||||
|
|
||||||
if (behavior == Behavior.Progressive || response.next == null) {
|
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 {
|
} else {
|
||||||
fetch(data)
|
channel.offer(Repository.Response(Repository.Origin.Network, data, true))
|
||||||
|
|
||||||
|
fetch(size + data.size)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
|
log(error.toString())
|
||||||
|
|
||||||
when (error.exception) {
|
when (error.exception) {
|
||||||
is RefreshError -> EventBus.send(Event.LogOut)
|
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> {
|
suspend fun get(url: String): Result<R, FuelError> {
|
||||||
log(url)
|
|
||||||
|
|
||||||
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
|
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
|
||||||
|
|
||||||
val (_, response, result) = Fuel
|
val (_, response, result) = Fuel
|
||||||
|
|
|
@ -12,7 +12,7 @@ import java.io.BufferedReader
|
||||||
|
|
||||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||||
override val cacheId = "tracks-playlist-$playlistId"
|
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 cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
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>() {
|
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||||
override val cacheId = "tracks-playlists"
|
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 cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
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.github.apognu.otter.utils.untilNetwork
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
interface Upstream<D> {
|
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>> {
|
abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||||
|
@ -19,7 +21,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||||
Network(0b10)
|
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 context: Context?
|
||||||
abstract val cacheId: String?
|
abstract val cacheId: String?
|
||||||
|
@ -35,29 +37,31 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||||
return _channel!!
|
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
|
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.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
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fromCache() {
|
private fun fromCache() {
|
||||||
cacheId?.let { cacheId ->
|
GlobalScope.launch(IO) {
|
||||||
Cache.get(context, cacheId)?.let { reader ->
|
cacheId?.let { cacheId ->
|
||||||
uncache(reader)?.let { cache ->
|
Cache.get(context, cacheId)?.let { reader ->
|
||||||
channel.offer(Response(Origin.Cache, cache.data))
|
uncache(reader)?.let { cache ->
|
||||||
|
channel.offer(Response(Origin.Cache, cache.data, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fromNetwork(from: List<D>) {
|
private fun fromNetwork(size: Int) {
|
||||||
upstream.fetch(data = from)?.untilNetwork(IO) {
|
upstream.fetch(size)?.untilNetwork(IO) { data, hasMore ->
|
||||||
val data = onDataFetched(it)
|
val data = onDataFetched(data)
|
||||||
|
|
||||||
cacheId?.let { cacheId ->
|
cacheId?.let { cacheId ->
|
||||||
Cache.set(
|
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>() {
|
class SearchRepository(override val context: Context?, query: String) : Repository<Track, TracksCache>() {
|
||||||
override val cacheId: String? = null
|
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 cache(data: List<Track>) = TracksCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
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>() {
|
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
||||||
override val cacheId = "tracks-album-$albumId"
|
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 cache(data: List<Track>) = TracksCache(data)
|
||||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
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 com.github.apognu.otter.Otter
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.GlobalScope
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Command {
|
sealed class Command {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.github.apognu.otter.utils
|
package com.github.apognu.otter.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
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) {
|
GlobalScope.launch(context) {
|
||||||
for (data in this@untilNetwork) {
|
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()
|
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"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/swiper"
|
|
||||||
style="@style/AppTheme.Fragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/scroller"
|
android:id="@+id/swiper"
|
||||||
|
style="@style/AppTheme.Fragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
tools:itemCount="10"
|
||||||
|
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
tools:listitem="@layout/row_album_grid"
|
||||||
|
tools:spanCount="3" />
|
||||||
|
|
||||||
<TextView
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
style="@style/AppTheme.Title"
|
|
||||||
|
<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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:orientation="vertical">
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:text="@string/albums" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<TextView
|
||||||
android:id="@+id/albums"
|
style="@style/AppTheme.Title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
tools:itemCount="10"
|
android:layout_marginStart="16dp"
|
||||||
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
android:layout_marginTop="16dp"
|
||||||
tools:listitem="@layout/row_album_grid"
|
android:layout_marginEnd="16dp"
|
||||||
tools:spanCount="3" />
|
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"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/swiper"
|
|
||||||
style="@style/AppTheme.Fragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/scroller"
|
android:id="@+id/swiper"
|
||||||
|
style="@style/AppTheme.Fragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
tools:listitem="@layout/row_artist" />
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
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.recyclerview.widget.RecyclerView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/artists"
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
tools:itemCount="10"
|
android:orientation="vertical">
|
||||||
tools:listitem="@layout/row_artist" />
|
|
||||||
|
|
||||||
</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"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/swiper"
|
|
||||||
style="@style/AppTheme.Fragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/scroller"
|
android:id="@+id/swiper"
|
||||||
|
style="@style/AppTheme.Fragment"
|
||||||
android:layout_width="match_parent"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
tools:listitem="@layout/row_track" />
|
||||||
android:orientation="vertical">
|
|
||||||
|
</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
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false">
|
android:clipChildren="false"
|
||||||
|
app:layout_collapseMode="parallax">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/AppTheme.Title"
|
style="@style/AppTheme.Title"
|
||||||
|
@ -48,13 +64,8 @@
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
android:id="@+id/favorites"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:listitem="@layout/row_track" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</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"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/swiper"
|
|
||||||
style="@style/AppTheme.Fragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/scroller"
|
android:id="@+id/swiper"
|
||||||
|
style="@style/AppTheme.Fragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical">
|
tools:itemCount="10"
|
||||||
|
tools:listitem="@layout/row_playlist" />
|
||||||
|
|
||||||
<TextView
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
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.recyclerview.widget.RecyclerView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/playlists"
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
tools:itemCount="10"
|
android:orientation="vertical">
|
||||||
tools:listitem="@layout/row_playlist" />
|
|
||||||
|
|
||||||
</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_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">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_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="settings_logout">Déconnexion</string>
|
||||||
|
|
||||||
<string name="artists">Artistes</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_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">Follow system settings</string>
|
||||||
<string name="settings_night_mode_system_summary">Night mode will 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="settings_logout">Sign out</string>
|
||||||
|
|
||||||
<string name="artists">Artists</string>
|
<string name="artists">Artists</string>
|
||||||
|
|
|
@ -39,6 +39,12 @@
|
||||||
android:key="oss_licences"
|
android:key="oss_licences"
|
||||||
android:title="@string/title_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
|
<Preference
|
||||||
android:icon="@drawable/logout"
|
android:icon="@drawable/logout"
|
||||||
android:key="logout"
|
android:key="logout"
|
||||||
|
|
Loading…
Reference in New Issue