-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add card browser chip filtering for flags #17355
base: main
Are you sure you want to change the base?
Changes from 1 commit
a5eda7d
800858b
735db72
d489f5b
c45fdd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,13 +110,12 @@ class CardBrowserViewModel( | |
|
||
val flowOfSearchState = MutableSharedFlow<SearchState>() | ||
|
||
var searchTerms = "" | ||
var searchTerms = SearchParameters.EMPTY | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am very confused by the fact that the variable is called |
||
private set | ||
private var restrictOnDeck: String = "" | ||
|
||
/** text in the search box (potentially unsubmitted) */ | ||
// this does not currently bind to the value in the UI and is only used for posting | ||
val flowOfFilterQuery = MutableSharedFlow<String>() | ||
val flowOfFilterQuery = MutableSharedFlow<SearchParameters>() | ||
|
||
/** | ||
* Whether the browser is working in Cards mode or Notes mode. | ||
|
@@ -193,16 +192,16 @@ class CardBrowserViewModel( | |
val lastDeckId: DeckId? | ||
get() = lastDeckIdRepository.lastDeckId | ||
|
||
suspend fun setDeckId(deckId: DeckId) { | ||
fun setDeckId(deckId: DeckId) { | ||
Timber.i("setting deck: %d", deckId) | ||
lastDeckIdRepository.lastDeckId = deckId | ||
restrictOnDeck = | ||
val newDeckRestriction = | ||
if (deckId == ALL_DECKS_ID) { | ||
"" | ||
setOf() | ||
} else { | ||
val deckName = withCol { decks.name(deckId) } | ||
"deck:\"$deckName\" " | ||
setOf(deckId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope nope nope nope. Please keep using the deck name instead of deck id. The meaning is very different. deck name also search for the cards in the subdecks. Deck id only search cards whose did is exactly this value. You were accidentally changing the semantic of selecting a deck in a way that would totally break the card browser for anyone with use of subdecks |
||
} | ||
searchTerms = searchTerms.copy(deckIds = newDeckRestriction) | ||
flowOfDeckId.update { deckId } | ||
} | ||
|
||
|
@@ -267,14 +266,14 @@ class CardBrowserViewModel( | |
var selectAllDecks = false | ||
when (options) { | ||
is CardBrowserLaunchOptions.SystemContextMenu -> { | ||
searchTerms = options.search.toString() | ||
searchTerms = searchTerms.copy(userInput = options.search.toString()) | ||
} | ||
is CardBrowserLaunchOptions.SearchQueryJs -> { | ||
searchTerms = options.search | ||
searchTerms = searchTerms.copy(userInput = options.search) | ||
selectAllDecks = options.allDecks | ||
} | ||
is CardBrowserLaunchOptions.DeepLink -> { | ||
searchTerms = options.search | ||
searchTerms = searchTerms.copy(userInput = options.search) | ||
} | ||
null -> {} | ||
} | ||
|
@@ -672,7 +671,7 @@ class CardBrowserViewModel( | |
/** Ignores any values before [initCompleted] is set */ | ||
private fun <T> Flow<T>.ignoreValuesFromViewModelLaunch(): Flow<T> = this.filter { initCompleted } | ||
|
||
suspend fun setFilterQuery(filterQuery: String) { | ||
suspend fun setFilterQuery(filterQuery: SearchParameters) { | ||
this.flowOfFilterQuery.emit(filterQuery) | ||
launchSearchForCards(filterQuery) | ||
} | ||
|
@@ -683,7 +682,7 @@ class CardBrowserViewModel( | |
suspend fun searchForMarkedNotes() { | ||
// only intended to be used if the user has no selection | ||
if (hasSelectedAnyRows()) return | ||
setFilterQuery("tag:marked") | ||
setFilterQuery(searchTerms.copy(userInput = "tag:marked")) | ||
} | ||
|
||
/** | ||
|
@@ -692,19 +691,20 @@ class CardBrowserViewModel( | |
suspend fun searchForSuspendedCards() { | ||
// only intended to be used if the user has no selection | ||
if (hasSelectedAnyRows()) return | ||
setFilterQuery("is:suspended") | ||
setFilterQuery(searchTerms.copy(userInput = "is:suspended")) | ||
} | ||
|
||
suspend fun setFlagFilter(flag: Flag) { | ||
Timber.i("filtering to flag: %s", flag) | ||
val flagSearchTerm = "flag:${flag.code}" | ||
val searchTerms = | ||
val userInput = searchTerms.userInput | ||
val updatedInput = | ||
when { | ||
searchTerms.contains("flag:") -> searchTerms.replaceFirst("flag:.".toRegex(), flagSearchTerm) | ||
searchTerms.isNotEmpty() -> "$flagSearchTerm $searchTerms" | ||
userInput.contains("flag:") -> userInput.replaceFirst("flag:.".toRegex(), flagSearchTerm) | ||
userInput.isNotEmpty() -> "$flagSearchTerm $userInput" | ||
else -> flagSearchTerm | ||
} | ||
setFilterQuery(searchTerms) | ||
setFilterQuery(searchTerms.copy(userInput = updatedInput)) | ||
} | ||
|
||
suspend fun filterByTags( | ||
|
@@ -717,7 +717,7 @@ class CardBrowserViewModel( | |
if (selectedTags.isNotEmpty()) { | ||
sb.append("($tagsConcat)") // Only if we added anything to the tag list | ||
} | ||
setFilterQuery(sb.toString()) | ||
setFilterQuery(searchTerms.copy(userInput = sb.toString())) | ||
} | ||
|
||
/** Previewing */ | ||
|
@@ -784,7 +784,7 @@ class CardBrowserViewModel( | |
*/ | ||
fun endMultiSelectMode() = selectNone() | ||
|
||
suspend fun launchSearchForCards(searchQuery: String): Job? { | ||
suspend fun launchSearchForCards(searchQuery: SearchParameters): Job? { | ||
searchTerms = searchQuery | ||
return launchSearchForCards() | ||
} | ||
|
@@ -798,12 +798,7 @@ class CardBrowserViewModel( | |
// update the UI while we're searching | ||
clearCardsList() | ||
|
||
val query: String = | ||
if (searchTerms.contains("deck:")) { | ||
"($searchTerms)" | ||
} else { | ||
if ("" != searchTerms) "$restrictOnDeck($searchTerms)" else restrictOnDeck | ||
} | ||
val query: String = searchTerms.toQuery() | ||
|
||
searchJob?.cancel() | ||
searchJob = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* This program is free software; you can redistribute it and/or modify it under | ||
* the terms of the GNU General Public License as published by the Free Software | ||
* Foundation; either version 3 of the License, or (at your option) any later | ||
* version. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY | ||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | ||
* PARTICULAR PURPOSE. See the GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License along with | ||
* this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package com.ichi2.anki.browser | ||
|
||
import android.os.Parcelable | ||
import com.ichi2.anki.Flag | ||
import com.ichi2.libanki.DeckId | ||
import kotlinx.parcelize.Parcelize | ||
|
||
@Parcelize | ||
data class SearchParameters( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd really appreciate some documentation. I admit I find it very clear, but I'm not certain it would be the case for a new contributor. In particular, every parameters comes from user input, whether they tap or type. I admit that I don't have a better value name. Still, it'd be nice to explain this corresponds to what the user typed in the search bar. It's expected to be a query but can actually be badly formed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Subject: [PATCH] [PATCH]
---
Index: AnkiDroid/src/main/java/com/ichi2/anki/browser/SearchParameters.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/SearchParameters.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/SearchParameters.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/SearchParameters.kt (revision c45fdd7d1dd576b6a9438ceb5721055a079140c8)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/SearchParameters.kt (date 1736379407730)
@@ -20,11 +20,24 @@
import com.ichi2.libanki.DeckId
import kotlinx.parcelize.Parcelize
+/**
+ * Represents the parameters displayed to the user in the card browser search UI. The search UI contains both the search text field where the user can type any query, and the chips.
+ * The result of this search consist in all cards satisfying all of the criteria below.
+ */
@Parcelize
data class SearchParameters(
val userInput: String,
+ /**
+ * Limit the search to cards belonging to any deck of [deckIds]. Note that it does not include any subdeck.
+ */
val deckIds: Set<DeckId>,
+ /**
+ * Limit the search to any note containing at least one tag in [tags].
+ */
val tags: Set<String>,
+ /**
+ * Limit the search to cards whose flag belong to [flags].
+ */
val flags: Set<Flag>,
) : Parcelable {
val isEmpty get() = userInput.isEmpty() && deckIds.isEmpty() && tags.isEmpty() && flags.isEmpty()
@@ -33,9 +46,8 @@
companion object {
val EMPTY = SearchParameters("", emptySet(), emptySet(), emptySet())
}
-}
-suspend fun SearchParameters.toQuery(): String {
+suspend fun toQuery(): String {
val targetDecksNames =
withCol {
deckIds.map { deckId -> decks.name(deckId) }
@@ -48,3 +60,4 @@
).filter { it.isNotEmpty() }
.joinToString(" ") { "($it)" }
}
+}
\ No newline at end of file |
||
val userInput: String, | ||
val deckIds: Set<DeckId>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume the reason why it's a set and not an arbitrary collection/iterable is that it ensures that it's parcelizable. If so, I'd suggest maybe adding a comment somewhere, because intuitively, it seems strange to me to restrict it to being a set specifically. |
||
val tags: Set<String>, | ||
val flags: Set<Flag>, | ||
) : Parcelable { | ||
val isEmpty get() = userInput.isEmpty() && deckIds.isEmpty() && tags.isEmpty() && flags.isEmpty() | ||
val isNotEmpty get() = !isEmpty | ||
|
||
companion object { | ||
val EMPTY = SearchParameters("", emptySet(), emptySet(), emptySet()) | ||
} | ||
} | ||
|
||
fun SearchParameters.toQuery() = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why define it as an extension instead of putting it into the class itself? |
||
listOf( | ||
userInput, | ||
deckIds.joinToString(" OR ") { "did:$it" }, | ||
tags.joinToString(" OR ") { """"tag:$it"""" }, | ||
flags.joinToString(" OR ") { "flag:${it.code}" }, | ||
).filter { it.isNotEmpty() } | ||
.joinToString(" ") { "($it)" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth adding a comment explaining that we're resetting only the text entered by the user, but keeping all chips as-is