feat(manage-tags): introduce ViewModel [DEV-only]#20578
feat(manage-tags): introduce ViewModel [DEV-only]#20578mikehardy merged 3 commits intoankidroid:mainfrom
Conversation
a341e75 to
7ab260b
Compare
| Timber.w("findVisibleNode called while not loaded") | ||
| return null | ||
| } | ||
| return loaded.visibleNodes.firstOrNull { it.fullTag == tag }.also { |
There was a problem hiding this comment.
findVisibleNode right now is O(n) , what if there are 20k tags? (hypothetical), can we think of doing O(1)?
There was a problem hiding this comment.
Trade off is memory though
There was a problem hiding this comment.
Even with 20k tags, this should be under a millisecond.
After this change, we rebuild the tree from the collection, so there's little point in this - we've got to pay somewhere to build the map
| private fun hasDescendantMatching( | ||
| node: BackendTagTreeNode, | ||
| parentFullTag: String, | ||
| searchQuery: String, | ||
| ): Boolean { | ||
| for (child in node.childrenList) { | ||
| val fullTag = "$parentFullTag::${child.name}" | ||
| if (fullTag.contains(searchQuery, ignoreCase = true)) return true | ||
| if (hasDescendantMatching(child, fullTag, searchQuery)) return true | ||
| } | ||
| return false | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
KDoc imo? And could we do a single pass instead? Compute all full paths once, mark matches,
and propagate "has matching descendant" back up to parents.
There was a problem hiding this comment.
I switched to a simple linear approach:
- build the flattened list once, with lowercased tag for performance (removes
ignoreCase = true) - Loop through the list and check. If a match occurs, flag all ancestors to be expanded.
This one has caused me multiple issues in the past Assisted-by: Claude Opus 4.6
Remove could be the number of tags, or the number of notes https://github.com/ankitects/anki/blob/2d44d4d6bc486803f9236033ad840df203c87036/rslib/src/tags/rename.rs https://github.com/ankitects/anki/blob/2d44d4d6bc486803f9236033ad840df203c87036/rslib/src/tags/remove.rs
5a2112f to
41223b2
Compare
* Based on tags.tree() * Delete/clear unused/rename are supported * Collapsed/expanded state is synced with the collection * A 'TagName' abstraction acts as a guardrail against logging tag names * Introduces a Compose-style 'Channel' for UI state notifications Part of issue 10397 Assisted-by: Claude Opus 4.6 Almost entirely written by Claude, with hours of back-and-forth over design I drove the ideas, Claude wrote the code
41223b2 to
8471769
Compare
mikehardy
left a comment
There was a problem hiding this comment.
dev-only, and foundation for more work (wherein I assume it'll be heavily exercised and if there are faults, they'll be flushed out). Can't think why not to merge it
lukstbit
left a comment
There was a problem hiding this comment.
Had some comments here but this was merged before coming back.
Mostly nitpicks but some real questions as well.
I was considering 'multi-delete', and I've decided against it.
The backend provides the functionality, but the potential UI states when combining it with a search/filter mechanism are too complex to explain well to the user:
I recommend to reconsider this. I can totally see some users using prefixes in tag names to group them and wanting to delete those in one go.
| data object Loading : ManageTagsState() | ||
|
|
||
| data class Loaded( | ||
| val visibleNodes: List<TagListItem>, |
There was a problem hiding this comment.
Why just the visible nodes?
Feels like we should just output all tags we have and make TagListItem have a boolean flag isFilteredOut to indicate its visibility. Should help with submitList as well if we used it in the UI.
We already have the full list of tags in the ViewModel and this should also simplify the tag filtering code.
There was a problem hiding this comment.
Is there a reason we'd ever want to display filtered nodes?
We'd want to filter them before they reached the RecyclerView for performance so we're only comparing deltas in visible rows, so I don't see how submitList would benefit.
I suppose we could get rid of the cache, but I don't find that compelling in itself, was there other simplification I'm missing?
Note
Assisted-by: Claude Opus 4.6
Almost entirely written by Claude, with hours of back-and-forth over design and refactorings
I drove the ideas, Claude wrote the code. Read it heavily
Note
Please request I break this up into commits if appropriate. 400LOC of well-commented non-test code is borderline to me.
Purpose / Description
Even though the UI of the tags dialog is subject to design, the ViewModel and basic operations which we will support are unlikely to change
tags.tree()TagNameabstraction acts as a guardrail against logging tag namesFixes
Approach
tag.tree()How Has This Been Tested?
Heavily unit tested
Learning
Channel<Event>pattern to consolidate all events in 1 flowOpChangesclasses at a later date and add a common marker interface, the lambda return bug has hit too many timesChecklist