Browse Source

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.

housekeeping/remove-warnings
Antoine POPINEAU 5 years ago
parent
commit
7c9a71d6d7
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
  1. 2
      app/build.gradle.kts
  2. 2
      app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt
  3. 2
      app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt
  4. 2
      app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt
  5. 11
      app/src/main/java/com/github/apognu/otter/activities/SettingsActivity.kt
  6. 8
      app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt
  7. 2
      app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt
  8. 2
      app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt
  9. 79
      app/src/main/java/com/github/apognu/otter/fragments/FunkwhaleFragment.kt
  10. 8
      app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt
  11. 1
      app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt
  12. 3
      app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt
  13. 4
      app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt
  14. 2
      app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt
  15. 6
      app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt
  16. 23
      app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt
  17. 2
      app/src/main/java/com/github/apognu/otter/repositories/PlaylistTracksRepository.kt
  18. 2
      app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt
  19. 30
      app/src/main/java/com/github/apognu/otter/repositories/Repository.kt
  20. 2
      app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt
  21. 2
      app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt
  22. 5
      app/src/main/java/com/github/apognu/otter/utils/EventBus.kt
  23. 7
      app/src/main/java/com/github/apognu/otter/utils/Extensions.kt
  24. 9
      app/src/main/res/drawable/experimental.xml
  25. 75
      app/src/main/res/layout/fragment_albums_grid.xml
  26. 70
      app/src/main/res/layout/fragment_artists.xml
  27. 49
      app/src/main/res/layout/fragment_favorites.xml
  28. 69
      app/src/main/res/layout/fragment_playlists.xml
  29. 4
      app/src/main/res/values-fr/strings.xml
  30. 4
      app/src/main/res/values/strings.xml
  31. 6
      app/src/main/res/xml/settings.xml

2
app/build.gradle.kts

@ -97,7 +97,7 @@ dependencies { @@ -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")

2
app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt

@ -64,7 +64,7 @@ class LoginActivity : AppCompatActivity() { @@ -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(

2
app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt

@ -256,7 +256,7 @@ class MainActivity : AppCompatActivity() { @@ -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)

2
app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt

@ -44,7 +44,7 @@ class SearchActivity : AppCompatActivity() { @@ -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

11
app/src/main/java/com/github/apognu/otter/activities/SettingsActivity.kt

@ -48,6 +48,17 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP @@ -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
app/src/main/java/com/github/apognu/otter/adapters/BrowseTabsAdapter.kt

@ -8,11 +8,17 @@ import com.github.apognu.otter.fragments.AlbumsGridFragment @@ -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 {

2
app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt

@ -29,7 +29,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() { @@ -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 ""
)
}
}

2
app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt

@ -17,8 +17,6 @@ class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() { @@ -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)

79
app/src/main/java/com/github/apognu/otter/fragments/FunkwhaleFragment.kt

@ -4,13 +4,19 @@ import android.os.Bundle @@ -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 @@ -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 @@ -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
swiper?.isRefreshing = true
if (size == 0) {
cleared = true
adapter.data.clear()
}
repository.fetch(upstreams, size).untilNetwork(IO) { data, hasMore ->
onDataFetched(data)
repository.fetch().untilNetwork {
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)
}
}
}
}

8
app/src/main/java/com/github/apognu/otter/fragments/PlaylistTracksFragment.kt

@ -99,9 +99,11 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd @@ -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)
}
}
}

1
app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt

@ -403,7 +403,6 @@ class PlayerService : Service() { @@ -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
app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt

@ -3,7 +3,6 @@ package com.github.apognu.otter.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) { @@ -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))
}

4
app/src/main/java/com/github/apognu/otter/repositories/AlbumsRepository.kt

@ -17,8 +17,8 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) : @@ -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,

2
app/src/main/java/com/github/apognu/otter/repositories/ArtistsRepository.kt

@ -11,7 +11,7 @@ import java.io.BufferedReader @@ -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)

6
app/src/main/java/com/github/apognu/otter/repositories/FavoritesRepository.kt

@ -14,7 +14,7 @@ import java.io.BufferedReader @@ -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 @@ -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 @@ -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)

23
app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt

@ -18,7 +18,7 @@ import java.io.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 @@ -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 @@ -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 @@ -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

2
app/src/main/java/com/github/apognu/otter/repositories/PlaylistTracksRepository.kt

@ -12,7 +12,7 @@ import java.io.BufferedReader @@ -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)

2
app/src/main/java/com/github/apognu/otter/repositories/PlaylistsRepository.kt

@ -11,7 +11,7 @@ import java.io.BufferedReader @@ -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)

30
app/src/main/java/com/github/apognu/otter/repositories/Repository.kt

@ -6,11 +6,13 @@ import com.github.apognu.otter.utils.CacheItem @@ -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>> { @@ -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>> { @@ -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>> { @@ -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))
}
}

2
app/src/main/java/com/github/apognu/otter/repositories/SearchRepository.kt

@ -12,7 +12,7 @@ import java.io.BufferedReader @@ -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)

2
app/src/main/java/com/github/apognu/otter/repositories/TracksRepository.kt

@ -12,7 +12,7 @@ import java.io.BufferedReader @@ -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)

5
app/src/main/java/com/github/apognu/otter/utils/EventBus.kt

@ -3,7 +3,10 @@ package com.github.apognu.otter.utils @@ -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 {

7
app/src/main/java/com/github/apognu/otter/utils/Extensions.kt

@ -1,6 +1,5 @@ @@ -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 = @@ -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()
}
}

9
app/src/main/res/drawable/experimental.xml

@ -0,0 +1,9 @@ @@ -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>

75
app/src/main/res/layout/fragment_albums_grid.xml

@ -1,47 +1,60 @@ @@ -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" />
<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" />
android:orientation="vertical">
<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>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</androidx.core.widget.NestedScrollView>
</com.google.android.material.appbar.AppBarLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

70
app/src/main/res/layout/fragment_artists.xml

@ -1,49 +1,59 @@ @@ -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.recyclerview.widget.RecyclerView
android:id="@+id/artists"
</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="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
tools:itemCount="10"
tools:listitem="@layout/row_artist" />
android:orientation="vertical">
<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" />
</LinearLayout>
</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>

49
app/src/main/res/layout/fragment_favorites.xml

@ -1,27 +1,43 @@ @@ -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 @@ @@ -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>

69
app/src/main/res/layout/fragment_playlists.xml

@ -1,49 +1,62 @@ @@ -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.recyclerview.widget.RecyclerView
android:id="@+id/playlists"
</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="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
tools:itemCount="10"
tools:listitem="@layout/row_playlist" />
android:orientation="vertical">
<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" />
</LinearLayout>
</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>

4
app/src/main/res/values-fr/strings.xml

@ -38,6 +38,10 @@ @@ -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>

4
app/src/main/res/values/strings.xml

@ -38,6 +38,10 @@ @@ -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>

6
app/src/main/res/xml/settings.xml

@ -39,6 +39,12 @@ @@ -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…
Cancel
Save