Filter favorites

This commit is contained in:
Ryan Harg 2022-12-09 08:49:41 +00:00
parent cf5d6a21fe
commit 87a0ef5a42
14 changed files with 180 additions and 20 deletions

View File

@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />

View File

@ -37,6 +37,7 @@ class FavoritesAdapter(
private lateinit var binding: RowTrackBinding
var currentTrack: Track? = null
var filter = ""
override fun getItemCount() = data.size
@ -44,6 +45,15 @@ class FavoritesAdapter(
return data[position].id.toLong()
}
override fun applyFilter() {
data.clear()
getUnfilteredData().map {
if (it.matchesFilter(filter)) {
data.add(it)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding = RowTrackBinding.inflate(layoutInflater, parent, false)

View File

@ -106,7 +106,7 @@ object AddToPlaylistDialog {
fetch().untilNetwork(lifecycleScope) { data, isCache, _, hasMore ->
if (isCache) {
adapter.data = data.toMutableList()
adapter.setUnfilteredData(data.toMutableList())
adapter.notifyDataSetChanged()
return@untilNetwork

View File

@ -18,12 +18,26 @@ import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class FFAAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var data: MutableList<D> = mutableListOf()
private var unfilteredData: MutableList<D> = mutableListOf()
fun getUnfilteredData(): MutableList<D> {
return unfilteredData
}
fun setUnfilteredData(data: MutableList<D>) {
unfilteredData = data
applyFilter()
}
open fun applyFilter() {
data.clear()
data.addAll(unfilteredData)
}
init {
super.setHasStableIds(true)
@ -130,19 +144,20 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
if (isCache) {
moreLoading = false
adapter.data = data.toMutableList()
adapter.setUnfilteredData(data.toMutableList())
adapter.notifyDataSetChanged()
return@launch
}
if (first) {
adapter.data.clear()
adapter.getUnfilteredData().clear()
}
onDataFetched(data)
adapter.data.addAll(data)
adapter.getUnfilteredData().addAll(data)
adapter.applyFilter()
withContext(IO) {
try {
@ -150,7 +165,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
FFACache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toString()
Gson().toJson(repository.cache(adapter.getUnfilteredData())).toString()
)
}
} catch (e: ConcurrentModificationException) {
@ -161,7 +176,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
if (first || needsMoreOffscreenPages()) {
fetch(Repository.Origin.Network.origin, adapter.data.size)
fetch(Repository.Origin.Network.origin, adapter.getUnfilteredData().size)
} else {
moreLoading = false
}

View File

@ -1,6 +1,8 @@
package audio.funkwhale.ffa.fragments
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -53,6 +55,20 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
): View {
_binding = FragmentFavoritesBinding.inflate(inflater)
swiper = binding.swiper
binding.filterTracks.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
adapter.applyFilter()
adapter.notifyDataSetChanged()
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
adapter.filter = s.toString()
}
})
return binding.root
}
@ -105,11 +121,13 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {
val data = adapter.data.map {
it.downloaded = downloaded.contains(it.id)
it
}.toMutableList()
adapter.setUnfilteredData(data)
adapter.notifyDataSetChanged()
}
}

View File

@ -92,7 +92,7 @@ class LandscapeQueueFragment : Fragment() {
activity?.lifecycleScope?.launch(Main) {
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
adapter?.let {
it.data = response.queue.toMutableList()
it.setUnfilteredData(response.queue.toMutableList())
it.notifyDataSetChanged()
if (it.data.isEmpty()) {

View File

@ -108,7 +108,7 @@ class QueueFragment : BottomSheetDialogFragment() {
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { response ->
binding.included.let { included ->
adapter?.let {
it.data = response.queue.toMutableList()
it.setUnfilteredData(response.queue.toMutableList())
it.notifyDataSetChanged()
if (it.data.isEmpty()) {

View File

@ -269,10 +269,12 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {
it.downloaded = downloaded.contains(it.id)
it
}.toMutableList()
adapter.setUnfilteredData(
adapter.data.map {
it.downloaded = downloaded.contains(it.id)
it
}.toMutableList()
)
adapter.notifyDataSetChanged()
}

View File

@ -1,5 +1,6 @@
package audio.funkwhale.ffa.model
import audio.funkwhale.ffa.utils.containsIgnoringCase
import com.preference.PowerPreference
data class Track(
@ -29,11 +30,13 @@ data class Track(
)
}
data class Upload(
val listen_url: String,
val duration: Int,
val bitrate: Int
)
data class Upload(val listen_url: String, val duration: Int, val bitrate: Int)
fun matchesFilter(filter: String): Boolean {
return title.containsIgnoringCase(filter) ||
artist.name.containsIgnoringCase(filter) ||
album?.title.containsIgnoringCase(filter)
}
override fun equals(other: Any?): Boolean {
return when (other) {

View File

@ -114,3 +114,6 @@ val ISO_8601_DATE_TIME_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
fun Date.format(): String {
return ISO_8601_DATE_TIME_FORMAT.format(this)
}
fun String?.containsIgnoringCase(candidate: String): Boolean =
this != null && this.lowercase().contains(candidate.lowercase())

View File

@ -39,14 +39,27 @@
android:clipChildren="false"
app:layout_collapseMode="parallax">
<EditText
android:id="@+id/filter_tracks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/favorites_title"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:inputType="text"
android:hint="@string/filters" />
<TextView
style="@style/AppTheme.Title"
android:id="@+id/favorites_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:layout_marginBottom="6dp"
android:text="@string/favorites" />
<com.google.android.material.button.MaterialButton

View File

@ -0,0 +1,64 @@
package audio.funkwhale.ffa.model
import org.junit.Test
import strikt.api.expectThat
import strikt.assertions.isFalse
import strikt.assertions.isTrue
class TrackTest {
@Test
fun trackMatchesTitle() {
expectThat(createTrackObject(trackTitle = "track").matchesFilter("track")).isTrue()
}
@Test
fun trackDoesntMatchTitle() {
expectThat(createTrackObject(trackTitle = "xxxx").matchesFilter("track")).isFalse()
}
@Test
fun trackMatchesArtist() {
expectThat(createTrackObject(artistName = "artist").matchesFilter("artist")).isTrue()
}
@Test
fun trackDoesntMatchArtist() {
expectThat(createTrackObject(artistName = "xxxx").matchesFilter("artist")).isFalse()
}
@Test
fun trackMatchesAlbum() {
expectThat(createTrackObject(albumTitle = "album").matchesFilter("album")).isTrue()
}
@Test
fun trackDoesntMatchAlbum() {
expectThat(createTrackObject(albumTitle = "xxxx").matchesFilter("album")).isFalse()
}
@Test
fun trackDoesntMatchNullAlbum() {
expectThat(createTrackObject(albumTitle = null).matchesFilter("album")).isFalse()
}
private fun createTrackObject(
trackTitle: String = "trackTitle",
artistName: String = "artistName",
albumTitle: String? = "albumTitle"
) = Track(
id = 0,
title = trackTitle,
artist = Artist(id = 0, name = artistName, albums = listOf()),
album =
if (albumTitle == null)
null
else Album(
id = 0,
title = albumTitle,
artist = Album.Artist("albumArtist"),
cover = null,
release_date = null
)
)
}

View File

@ -0,0 +1,30 @@
package audio.funkwhale.ffa.utils
import org.junit.Test
import strikt.api.expectThat
import strikt.assertions.isFalse
import strikt.assertions.isTrue
internal class ExtensionsKtTest {
@Test
fun nullStringDoesntContainCandidate() {
val s: String? = null
expectThat(s.containsIgnoringCase("candidate")).isFalse()
}
@Test
fun stringDoesntContainCandidate() {
expectThat("string".containsIgnoringCase("candidate")).isFalse()
}
@Test
fun sameStringWithDifferentCasingContainsCandidate() {
expectThat("CANDIDATE".containsIgnoringCase("candidate")).isTrue()
}
@Test
fun sameStringWithMatchingCasingContainsCandidate() {
expectThat("candidate".containsIgnoringCase("candidate")).isTrue()
}
}

View File

@ -0,0 +1 @@
Add filtering functionality to favorites view (thanks @PhieF)