-
-
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?
Conversation
Important Maintainers: This PR contains Strings changes
|
fa1f5c9
to
e34147f
Compare
I fixed the conflicts for this. Note: I also did the filtering for card states but I didn't add the PR yet because it sits on top of this. |
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.
Clicking on the flag icons has no effect other than a ripple
Also for the X in the 'clear filter'
Clear filter should allow a user to click anywhere in the row, as with the flag icons, rather than just the text
Other than that, THANK YOU SO MUCH! Looks great
val userInput: String, | ||
val deckIds: Set<DeckId>, | ||
val tags: Set<String>, | ||
val flags: Set<Flag> | ||
) : Parcelable { | ||
val isEmpty get() = userInput.isEmpty() && deckIds.isEmpty() && tags.isEmpty() && flags.isEmpty() | ||
val isNotEmpty get() = !isEmpty |
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.
This seems like too many parameters, is tags
necessary
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.
Yes, we plan to add a chip for filtering by tags(or so it was done in your original PR).
AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
@lukstbit is this still awaiting comment resolution, and can I help at all? Would love to see it in |
e34147f
to
00c8f0e
Compare
Sorry, I was just busy and didn't had time to update the code. |
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'm so sorry, didn't realize this was open for review! This is great, one blocker
design nitpicks, non-blocking
- If 'clear' is not visible, the top of the sheet could do with a few more dp blank space
- Could do with a few dp padding to the end of the flag icon
- I /think/ the font may be a little larger/bolder than gmail and could be reduced by a few dp
00c8f0e
to
b3044dc
Compare
b3044dc
to
3012fb7
Compare
3012fb7
to
0581fb8
Compare
TODO: Thank Oakkitten for the code Cherry-picked from ankidroid#16859, commit 2b89111 There was a conflict related to imports in card browser that was fixed.
This fixes the bug ankidroid@2b89111#r1731116500 where using a top level deck id in the search query will result in an empty result set although its child decks do have cards. The fix consists in properly creating the search query by switching from "did" to "deck" for deck selection(ex did:435435 -> deck:"Deck name"). Looking though the docs, it seems "did" isn't a valid selector for a deck and the selector "deck" must be used instead. Note that the deck name is put in "" to accommodate decks with spaces in name. Note: I choose to make toQuery() suspended to access the collection for the decks names to avoid SearchParameters referencing the decks names and then having to synchronize with outside name changes.
Cherry-picked from ankidroid#16859, commit 6565924 + other changes Other changes: - renamed bottom_sheet_item -> item_browser_flag_filter - refactor item_browser_flag_filter to simplify and remove unused views - removed the BottomSheetFragment, TreeAdapter and TemplatedTreeAdapter classes as they will not be used for flags filtering - removed bg_input.xml and bottom_sheet_list_without_filter.xml as they are unused - removed the chip for flags filtering, it will be added in a separate commit - moved BottomSheetDialogFragmentFix to the anki.workarounds package, I assumed we might want this in other places as well - renamed the id of the ChipGroup from chips_group to filtering_chips_group
Code partially cherry-picked from ankidroid#16859, commit 4254250 References to flags filtering were removed and will(partially) be added in the next commit. The code works now as before related to flags filtering.
The flags filtering was moved from a menu option to an embedded chip view triggering a separate bottom sheet dialog. Contains some code from commit 4254250 that was about flags filtering. Note: see the changes in CardBrowserTest.checkIfLongSelectChecksAllCardsInBetween(). For some reason after adding the tests related to flag filtering, this test started to throw NullPointerExceptions which also triggered another test to crash as well due to unhandled exception from another test. Using a position that should be displayed seems to fix this.
0581fb8
to
c45fdd7
Compare
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.
Apart from the one comment I left asking for a more detailed comment, the code looks good.
I'm a little curious about the behavior though - specifically, the flags chips are in effect performing a search. But we also have a search box, and they don't seem to be connected
Consider: you have 2 cards flagged red (flag:1), 2 cards flagged orange (flag:2)
Anki default search behavior is to have an invisible/implicit "and" between terms.
section "Things to note from above" in https://docs.ankiweb.net/searching.html#simple-searches
The chip behavior here is an implicit "or" on all flags - if I select red and orange I see 4 cards now.
The search box is an "and" but if I search for "flag:1 or flag:2" I will see 4 cards.
However, if I search for "flag:1 and flag:2" while I have the red chip selected, I only see the 2 red cards, that is I am filtering again - it appears to be a two-layer filter - search box then chips.
So - with that example I think I show that it appears to be a two-step filter now - the search box then the chips. Is that what we want?
Or do we want whatever you selected in the chips to be reflected in the search box if you open it? And for whatever chip-related items you select in the search box to update the state of the chips?
Seems like more work to connect chips and search box but also then reduces it to a single-level filter, and allows users to save chip filter setups in saved searches as they would be reflected as a real search in the search box
🤔 ?
import com.google.android.material.bottomsheet.BottomSheetDialogFragment | ||
import com.ichi2.anki.convertDpToPixel | ||
|
||
// https://stackoverflow.com/questions/41591733 |
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.
The stackoverflow link is good and all, but some text describing the issue is better than
making folks click through and wade through a bunch of stackoverflow stuff
// https://stackoverflow.com/questions/41591733 | |
// Android material library BottomSheet has a bug where it is not expanded in landscape mode | |
// So any time we load the bottom sheet or the configuration changes, we check if we are in | |
// landscape mode, and if so, we force the sheet to be an expanded height | |
// https://stackoverflow.com/questions/41591733 |
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.
Thank you very much to you, and @oakkitten who started this long ago. Looks really nice.
I only had time to look at a single commit. Only small nits, but I love it
} | ||
} | ||
|
||
fun SearchParameters.toQuery() = |
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.
Why define it as an extension instead of putting it into the class itself?
That's not wrong, but seems non standard and unexpected
@@ -931,7 +932,7 @@ open class CardBrowser : | |||
viewModel.setSearchQueryExpanded(false) | |||
// SearchView doesn't support empty queries so we always reset the search when collapsing | |||
searchView!!.setQuery("", false) | |||
searchCards("") | |||
searchCards(viewModel.searchTerms.copy(userInput = "")) |
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
import kotlinx.parcelize.Parcelize | ||
|
||
@Parcelize | ||
data class SearchParameters( |
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'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 comment
The 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
@Parcelize | ||
data class SearchParameters( | ||
val userInput: String, | ||
val deckIds: Set<DeckId>, |
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 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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I am very confused by the fact that the variable is called searchTerms
and the type SearchParameters
.
I understand keeping searchTerms
avoid to change 37 lines of code. Still, I think it'd be nicer to be consistent between the variable and the type name. Unless there is a nuance I'm missing
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.
My bad, there was one subtle breaking change here.
I'd appreciate if you maybe could add a regression test to ensure that we would catch in the future if a search stopped showing cards in subdeck of selected deck
import kotlinx.parcelize.Parcelize | ||
|
||
@Parcelize | ||
data class SearchParameters( |
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.
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
} else { | ||
val deckName = withCol { decks.name(deckId) } | ||
"deck:\"$deckName\" " | ||
setOf(deckId) |
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.
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
I think what you should compare this feature too is what occurs if you use the entries in the left menu of anki card browser.
I'd argue for a strong yes. To state it another way. If they just want a simple search, to find a few specific cards, this interface is quite simpler, so that's great. If they really need the full query language to do complex search, anyway, I think the complex part is learning how to do boolean combination of search terms, and understanding the logic of this extra "and" is probably easy
Strong no for me. The query can be absurdly complex. There is no way that we'd have a clear meaning for what it means to unselect a flag when the flag restriction is deep in the query. And reciprocally, if the flag restriction is not at the top level of the AST, it's not clear whether or not you want to select the flag in the chip. It's even worse if you use a query of the kind "-flag:0". If we ever get complaints that we don't offer enough flexibility and somewhat have a nice answer, maybe let's reconsider it. I think this will be a nightmare to do, and there will never be a right solution for the problem you want to solve |
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.
Well-reasoned by @Arthur-Milchior, thanks. I yield ;-) - +1
@snowtimeglass a crossed out flag (vs an empty one) for "no flag" might be nice but I think that could be separate I think if the main search query language is "and" combination, perhaps we should follow that. I'm trying to recall filter systems I use - not many unfortunately - and the filters are all "and" if you have multiple in the ones I can think of. |
I am once again asking (insert Bernie.jpg) to use more symmetrical padding (horizontal and vertical). (Also between items?) Also, I again suggest to not use “x” as an icon to clear filters. I understand the reasoning for “or” but if we think about other possible chips, we probably don't want to do AND anywhere inside them. Do we want to have the word “or” for decks or tags? I will not be trying to judge how easy it would be to grasp the chips for the end user, but if we do AND for chips and OR for items inside chips, the overall design would be at the very least describable as not complex. So I'd opt for the universal +1 for simplicity (and note no space!). Also, don't forget that flag names are customizable by the user.
Also, there's too much space between chips and sort/type selectors. Also, the browser header now takes something like ¼ of my screen. Can we block chips until the header is collapsible on scrolling?
I think that with chips users will expect 2 levels. Reflecting it in the search box would be good for power users—if they want to negate things, or change parameters for things with parameters. But:
We could do something like a one-way conversion from chips to query string, with hiding the chip bar. |
Also this crashes on dark mode change (I actually bothered to run the code!)
|
val targetDecksNames = | ||
withCol { | ||
deckIds.map { deckId -> decks.name(deckId) } | ||
} |
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.
Can we change SearchParameters
to use deck names in the first place? If not, I suggest adding a comment in the code regarding the necessity of this, instead of using the commit message for this.
I appreciate you all listening to my semi-vague questions about syncing search bar and chips (Arthur) and the and-vs-or question I had (snowtimeglass and oakkitten) Well reasoned responses on and-vs-or, seems like "or" might be the only way to do it that makes sense (c.f. oakkitten) and it has usage that way in gmail which everyone uses (c.f. snowtimeglass) Okay then, "or" on chips is fine by me. I don't have any other musings. I did also run the code myself and really played with it - just didn't try a theme switch like oakkitten did. Appears that's an issue but I didn't see anything that would take away my +1 |
Sorry, disregard that bit, I did a silly |
Purpose / Description
Adds a part of the CardBrowser new chip filtering, targeting flags. The PR uses code from the original attempt at #16859 where possible. See more info in the messages of commits.
UI note: I think it would be better if we just show Flags +x instead of FlagName +1:
Some images:
Fixes
How Has This Been Tested?
Ran the tests and filtered by flag.
Checklist