Skip to content

Commit 647d099

Browse files
committed
Add support of filtering torrents by label
1 parent 5e288e5 commit 647d099

File tree

6 files changed

+130
-7
lines changed

6 files changed

+130
-7
lines changed

app/src/main/kotlin/org/equeim/tremotesf/ui/Settings.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ object Settings {
315315
mappedToPrefs = { it.ordinal }
316316
)
317317

318+
val torrentsLabelFilter: Property<String> = PrefsProperty(
319+
R.string.torrents_label_filter_key,
320+
R.string.torrents_label_filter_default_value
321+
)
322+
318323
val torrentsTrackerFilter: Property<String> = PrefsProperty(
319324
R.string.torrents_tracker_filter_key,
320325
R.string.torrents_tracker_filter_default_value
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-FileCopyrightText: 2017-2025 Alexey Rochev <[email protected]>
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package org.equeim.tremotesf.ui.torrentslistfragment
6+
7+
import android.content.Context
8+
import android.widget.AutoCompleteTextView
9+
import org.equeim.tremotesf.R
10+
import org.equeim.tremotesf.common.AlphanumericComparator
11+
import org.equeim.tremotesf.rpc.requests.Torrent
12+
import org.equeim.tremotesf.ui.utils.AutoCompleteTextViewDynamicAdapter
13+
14+
class LabelsViewAdapter(
15+
private val context: Context,
16+
textView: AutoCompleteTextView,
17+
) : AutoCompleteTextViewDynamicAdapter(textView) {
18+
private data class LabelItem(val label: String?, val torrents: Int)
19+
20+
private var labels = emptyList<LabelItem>()
21+
22+
private val comparator = object : Comparator<LabelItem> {
23+
val labelComparator = AlphanumericComparator()
24+
override fun compare(o1: LabelItem, o2: LabelItem): Int =
25+
labelComparator.compare(o1.label, o2.label)
26+
}
27+
28+
private var currentLabelIndex: Int = 0
29+
30+
override fun getItem(position: Int): String {
31+
val item = labels.getOrNull(position) ?: return ""
32+
return if (item.label != null) {
33+
context.getString(
34+
R.string.directories_spinner_text,
35+
item.label,
36+
item.torrents
37+
)
38+
} else {
39+
context.getString(R.string.torrents_all, item.torrents)
40+
}
41+
}
42+
43+
override fun getCount(): Int {
44+
return labels.size
45+
}
46+
47+
override fun getCurrentItem(): CharSequence {
48+
return getItem(currentLabelIndex)
49+
}
50+
51+
fun getLabel(position: Int): String? {
52+
return labels[position].label
53+
}
54+
55+
fun update(torrents: List<Torrent>, labelFilter: String) {
56+
labels = torrents
57+
.asSequence()
58+
.flatMap { torrent -> torrent.labels.asSequence().map { torrent to it } }
59+
.groupingBy { (_, label) -> label }
60+
.eachCount()
61+
.mapTo(mutableListOf(LabelItem(null, torrents.size))) { (label, torrents) ->
62+
LabelItem(
63+
label,
64+
torrents
65+
)
66+
}
67+
.apply { sortWith(comparator) }
68+
currentLabelIndex = if (labelFilter.isEmpty()) {
69+
0
70+
} else {
71+
labels.indexOfFirst { it.label == labelFilter }.takeUnless { it == -1 } ?: 0
72+
}
73+
notifyDataSetChanged()
74+
}
75+
}

app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsFiltersDialogFragment.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ class TorrentsFiltersDialogFragment : NavigationBottomSheetDialogFragment(R.layo
5757
}
5858
}
5959

60+
val labelsViewAdapter = LabelsViewAdapter(requireContext(), labelsView)
61+
labelsView.apply {
62+
setAdapter(labelsViewAdapter)
63+
setOnItemClickListener { _, _, position, _ ->
64+
model.setLabelFilter(labelsViewAdapter.getLabel(position).orEmpty())
65+
}
66+
}
67+
6068
val trackersViewAdapter = TrackersViewAdapter(requireContext(), trackersView)
6169
trackersView.apply {
6270
setAdapter(trackersViewAdapter)
@@ -85,6 +93,11 @@ class TorrentsFiltersDialogFragment : NavigationBottomSheetDialogFragment(R.layo
8593
statusFilterViewAdapter.update(torrents, statusFilterMode)
8694
}
8795

96+
combine(allTorrents, model.labelFilter, ::Pair)
97+
.launchAndCollectWhenStarted(viewLifecycleOwner) { (torrents, labelFilter) ->
98+
labelsViewAdapter.update(torrents, labelFilter)
99+
}
100+
88101
combine(allTorrents, model.trackerFilter, ::Pair)
89102
.launchAndCollectWhenStarted(viewLifecycleOwner) { (torrents, trackerFilter) ->
90103
trackersViewAdapter.update(torrents, trackerFilter)

app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragmentViewModel.kt

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
137137
fun setSortOrder(order: SortOrder) = Settings.torrentsSortOrder.setAsync(order)
138138
val statusFilterMode: Flow<StatusFilterMode> = Settings.torrentsStatusFilter.flow()
139139
fun setStatusFilterMode(mode: StatusFilterMode) = Settings.torrentsStatusFilter.setAsync(mode)
140+
val labelFilter: Flow<String> = Settings.torrentsLabelFilter.flow()
141+
fun setLabelFilter(filter: String) = Settings.torrentsLabelFilter.setAsync(filter)
140142
val trackerFilter: Flow<String> = Settings.torrentsTrackerFilter.flow()
141143
fun setTrackerFilter(filter: String) = Settings.torrentsTrackerFilter.setAsync(filter)
142144
val directoryFilter: Flow<String> = Settings.torrentsDirectoryFilter.flow()
@@ -152,6 +154,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
152154
setSortMode(SortMode.DEFAULT)
153155
setSortOrder(SortOrder.DEFAULT)
154156
setStatusFilterMode(StatusFilterMode.DEFAULT)
157+
setLabelFilter("")
155158
setTrackerFilter("")
156159
setDirectoryFilter("")
157160
}
@@ -161,14 +164,20 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
161164
sortMode,
162165
sortOrder,
163166
statusFilterMode,
167+
labelFilter,
164168
trackerFilter,
165169
directoryFilter,
166-
) { (sortMode, sortOrder, statusFilterMode, trackerFilter, directoryFilter) ->
167-
sortMode != SortMode.DEFAULT ||
168-
sortOrder != SortOrder.DEFAULT ||
169-
statusFilterMode != StatusFilterMode.DEFAULT ||
170-
(trackerFilter as String).isNotEmpty() ||
171-
(directoryFilter as String).isNotEmpty()
170+
) { settings ->
171+
settings.any {
172+
when (it) {
173+
is SortMode -> it != SortMode.DEFAULT
174+
is SortOrder -> it != SortOrder.DEFAULT
175+
is StatusFilterMode -> it != StatusFilterMode.DEFAULT
176+
is String -> it.isNotEmpty()
177+
// Shouldn't be possible
178+
else -> throw IllegalStateException("Unknown filter value $filters")
179+
}
180+
}
172181
}
173182

174183
private val refreshRequests = MutableSharedFlow<Unit>()
@@ -386,6 +395,7 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
386395
private fun Flow<RpcRequestState<List<Torrent>>>.filterAndSortTorrents(): Flow<RpcRequestState<List<Torrent>>> {
387396
val filterPredicateFlow = combine(
388397
statusFilterMode,
398+
labelFilter,
389399
trackerFilter,
390400
directoryFilter,
391401
nameFilter.flow(),
@@ -406,13 +416,15 @@ class TorrentsListFragmentViewModel(application: Application, savedStateHandle:
406416

407417
private fun createFilterPredicate(
408418
statusFilterMode: StatusFilterMode,
419+
labelFilter: String,
409420
trackerFilter: String,
410421
directoryFilter: String,
411422
nameFilter: String,
412423
): (Torrent) -> Boolean {
413424
return { torrent: Torrent ->
414425
statusFilterAcceptsTorrent(torrent, statusFilterMode) &&
415-
(trackerFilter.isEmpty() || (torrent.trackerSites.find { it == trackerFilter } != null)) &&
426+
(labelFilter.isEmpty() || torrent.labels.contains(labelFilter)) &&
427+
(trackerFilter.isEmpty() || (torrent.trackerSites.contains(trackerFilter))) &&
416428
(directoryFilter.isEmpty() || torrent.downloadDirectory.value == directoryFilter) &&
417429
torrent.name.contains(nameFilter, true)
418430
}

app/src/main/res/layout/torrents_filters_dialog_fragment.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ SPDX-License-Identifier: GPL-3.0-or-later
8080
app:readOnly="true" />
8181
</com.google.android.material.textfield.TextInputLayout>
8282

83+
<com.google.android.material.textfield.TextInputLayout
84+
android:id="@+id/labels_view_layout"
85+
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
86+
android:layout_width="match_parent"
87+
android:layout_height="wrap_content"
88+
android:layout_marginTop="@dimen/linear_layout_vertical_spacing"
89+
android:hint="@string/labels">
90+
91+
<org.equeim.tremotesf.ui.views.NonFilteringAutoCompleteTextView
92+
android:id="@+id/labels_view"
93+
android:layout_width="match_parent"
94+
android:layout_height="wrap_content"
95+
app:readOnly="true" />
96+
</com.google.android.material.textfield.TextInputLayout>
97+
8398
<com.google.android.material.textfield.TextInputLayout
8499
android:id="@+id/trackers_view_layout"
85100
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"

app/src/main/res/values/prefs_constants.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
8787
<string name="torrents_status_filter_key" translatable="false">torrentsStatusFilter</string>
8888
<integer name="torrents_status_filter_default_value">-1</integer>
8989

90+
<string name="torrents_label_filter_key" translatable="false">torrentsLabelFilter</string>
91+
<string name="torrents_label_filter_default_value" translatable="false" />
92+
9093
<string name="torrents_tracker_filter_key" translatable="false">torrentsTrackerFilter</string>
9194
<string name="torrents_tracker_filter_default_value" translatable="false" />
9295

0 commit comments

Comments
 (0)