Filter favorites
This commit is contained in:
parent
cf5d6a21fe
commit
87a0ef5a42
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Add filtering functionality to favorites view (thanks @PhieF)
|
Loading…
Reference in New Issue