Fixed download and cache indicators on search screen. Fixed an issue with placeholder texts when some search terms did not return results.

This commit is contained in:
Antoine POPINEAU 2020-06-22 21:48:31 +02:00
parent 08a7a28c22
commit 03fcf1a382
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
8 changed files with 116 additions and 62 deletions

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.DownloadsAdapter import com.github.apognu.otter.adapters.DownloadsAdapter
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
@ -45,11 +46,12 @@ class DownloadsActivity : AppCompatActivity() {
private fun refresh() { private fun refresh() {
GlobalScope.launch(Main) { GlobalScope.launch(Main) {
RequestBus.send(Request.GetDownloads).wait<Response.Downloads>()?.let { response -> val cursor = Otter.get().exoDownloadManager.downloadIndex.getDownloads()
adapter.downloads.clear() adapter.downloads.clear()
while (response.cursor.moveToNext()) { while (cursor.moveToNext()) {
val download = response.cursor.download val download = cursor.download
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->
adapter.downloads.add(info.apply { adapter.downloads.add(info.apply {
@ -61,7 +63,6 @@ class DownloadsActivity : AppCompatActivity() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }
}
private suspend fun refreshTrack(download: Download) { private suspend fun refreshTrack(download: Download) {
download.getMetadata()?.let { info -> download.getMetadata()?.let { info ->

View File

@ -25,6 +25,8 @@ class SearchActivity : AppCompatActivity() {
lateinit var favoritesRepository: FavoritesRepository lateinit var favoritesRepository: FavoritesRepository
var done = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -46,6 +48,8 @@ class SearchActivity : AppCompatActivity() {
search.clearFocus() search.clearFocus()
rawQuery?.let { rawQuery?.let {
done = 0
val query = URLEncoder.encode(it, "UTF-8") val query = URLEncoder.encode(it, "UTF-8")
tracksRepository = TracksSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT)) tracksRepository = TracksSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
@ -54,6 +58,7 @@ class SearchActivity : AppCompatActivity() {
favoritesRepository = FavoritesRepository(this@SearchActivity) favoritesRepository = FavoritesRepository(this@SearchActivity)
search_spinner.visibility = View.VISIBLE search_spinner.visibility = View.VISIBLE
search_empty.visibility = View.GONE
search_no_results.visibility = View.GONE search_no_results.visibility = View.GONE
adapter.artists.clear() adapter.artists.clear()
@ -62,33 +67,24 @@ class SearchActivity : AppCompatActivity() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork { artists, _, _ -> artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork { artists, _, _ ->
when (artists.isEmpty()) { done++
true -> search_no_results.visibility = View.VISIBLE
false -> adapter.artists.addAll(artists)
}
adapter.notifyDataSetChanged() adapter.artists.addAll(artists)
refresh()
} }
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork { albums, _, _ -> albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork { albums, _, _ ->
when (albums.isEmpty()) { done++
true -> search_no_results.visibility = View.VISIBLE
false -> adapter.albums.addAll(albums)
}
adapter.notifyDataSetChanged() adapter.albums.addAll(albums)
refresh()
} }
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks, _, _ -> tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork { tracks, _, _ ->
search_spinner.visibility = View.GONE done++
search_empty.visibility = View.GONE
when (tracks.isEmpty()) { adapter.tracks.addAll(tracks)
true -> search_no_results.visibility = View.VISIBLE refresh()
false -> adapter.tracks.addAll(tracks)
}
adapter.notifyDataSetChanged()
} }
} }
@ -99,6 +95,20 @@ class SearchActivity : AppCompatActivity() {
}) })
} }
private fun refresh() {
adapter.notifyDataSetChanged()
if (adapter.artists.size + adapter.albums.size + adapter.tracks.size == 0) {
search_no_results.visibility = View.VISIBLE
} else {
search_no_results.visibility = View.GONE
}
if (done == 3) {
search_spinner.visibility = View.INVISIBLE
}
}
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener { inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
override fun onArtistClick(holder: View?, artist: Artist) { override fun onArtistClick(holder: View?, artist: Artist) {
ArtistsFragment.openAlbums(this@SearchActivity, artist) ArtistsFragment.openAlbums(this@SearchActivity, artist)

View File

@ -2,6 +2,8 @@ package com.github.apognu.otter.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build import android.os.Build
import android.view.Gravity import android.view.Gravity
@ -69,10 +71,6 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
return ResultType.Track.ordinal return ResultType.Track.ordinal
} }
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = when (viewType) { val view = when (viewType) {
ResultType.Header.ordinal -> LayoutInflater.from(context).inflate(R.layout.row_search_header, parent, false) ResultType.Header.ordinal -> LayoutInflater.from(context).inflate(R.layout.row_search_header, parent, false)
@ -93,27 +91,33 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
if (position == 0) { if (position == 0) {
holder.title.text = context.getString(R.string.artists) holder.title.text = context.getString(R.string.artists)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
if (artists.isEmpty()) { if (artists.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
} }
} }
if (position == (artists.size + 1)) { if (position == (artists.size + 1)) {
holder.title.text = context.getString(R.string.albums) holder.title.text = context.getString(R.string.albums)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
if (albums.isEmpty()) { if (albums.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
} }
} }
if (position == (artists.size + albums.size + 2)) { if (position == (artists.size + albums.size + 2)) {
holder.title.text = context.getString(R.string.tracks) holder.title.text = context.getString(R.string.tracks)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
holder.itemView.visibility = View.GONE holder.itemView.visibility = View.GONE
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
} }
} }
} }
@ -160,6 +164,8 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
holder.artist.typeface = Typeface.create(holder.artist.typeface, Typeface.NORMAL) holder.artist.typeface = Typeface.create(holder.artist.typeface, Typeface.NORMAL)
}) })
holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
if (resultType == ResultType.Track.ordinal) { if (resultType == ResultType.Track.ordinal) {
(item as? Track)?.let { track -> (item as? Track)?.let { track ->
context?.let { context -> context?.let { context ->
@ -183,6 +189,23 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
} }
} }
when (track.cached || track.downloaded) {
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
if (track.cached && !track.downloaded) {
holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
}
}
if (track.downloaded) {
holder.title.compoundDrawables.forEach {
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
}
}
holder.actions.setOnClickListener { holder.actions.setOnClickListener {
PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply { PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
inflate(R.menu.row_track) inflate(R.menu.row_track)

View File

@ -1,6 +1,7 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.Otter
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.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -22,8 +23,18 @@ class TracksSearchRepository(override val context: Context?, query: String) : Re
.toList() .toList()
.flatten() .flatten()
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
data.map { track -> data.map { track ->
track.favorite = favorites.contains(track.id) track.favorite = favorites.contains(track.id)
track.downloaded = downloaded.contains(track.id)
track.bestUpload()?.let { upload ->
val url = mustNormalizeUrl(upload.listen_url)
track.cached = Otter.get().exoCache.isCached(url, 0, upload.duration * 1000L)
}
track track
} }
} }

View File

@ -19,12 +19,12 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
companion object { companion object {
suspend fun getDownloadedIds(): List<Int>? { fun getDownloadedIds(): List<Int>? {
return RequestBus.send(Request.GetDownloads).wait<com.github.apognu.otter.utils.Response.Downloads>()?.let { response -> val cursor = Otter.get().exoDownloadManager.downloadIndex.getDownloads()
val ids: MutableList<Int> = mutableListOf() val ids: MutableList<Int> = mutableListOf()
while (response.cursor.moveToNext()) { while (cursor.moveToNext()) {
val download = response.cursor.download val download = cursor.download
download.getMetadata()?.let { download.getMetadata()?.let {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
@ -33,8 +33,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
} }
} }
ids return ids
}
} }
} }

View File

@ -4,7 +4,6 @@ import com.github.apognu.otter.Otter
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadCursor import com.google.android.exoplayer2.offline.DownloadCursor
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@ -89,7 +88,7 @@ object CommandBus {
object RequestBus { object RequestBus {
fun send(request: Request): Channel<Response> { fun send(request: Request): Channel<Response> {
return Channel<Response>().also { return Channel<Response>().also {
GlobalScope.launch(Main) { GlobalScope.launch(IO) {
request.channel = it request.channel = it
Otter.get().requestBus.offer(request) Otter.get().requestBus.offer(request)

View File

@ -18,9 +18,17 @@
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="0dp"
android:elevation="4dp"> android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/search" android:id="@+id/search"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -29,16 +37,19 @@
app:queryBackground="@android:color/transparent" app:queryBackground="@android:color/transparent"
app:queryHint="@string/search_placeholder" /> app:queryHint="@string/search_placeholder" />
</androidx.cardview.widget.CardView>
<ProgressBar <ProgressBar
android:id="@+id/search_spinner" android:id="@+id/search_spinner"
android:layout_width="32dp" style="?android:attr/progressBarStyleHorizontal"
android:layout_height="32dp" android:layout_width="match_parent"
android:layout_gravity="center" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="-12dp"
android:layout_marginBottom="-12dp"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" /> android:visibility="invisible" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView <TextView
android:id="@+id/search_empty" android:id="@+id/search_empty"

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:background="@drawable/ripple"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:transitionGroup="true" android:transitionGroup="true"