diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 379f50e1e..639f8cb88 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -31,19 +31,19 @@ from anki.rsbackend import ( # pylint: disable=unused-import
ConcatSeparator,
DBError,
DupeIn,
- FilterToSearchIn,
+ Flag,
FormatTimeSpanContext,
InvalidInput,
- NamedFilter,
NoteIDs,
Progress,
RustBackend,
+ SearchTerm,
pb,
)
from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler
from anki.tags import TagManager
-from anki.utils import devMode, ids2str, intTime
+from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia
if TYPE_CHECKING:
from anki.rsbackend import FormatTimeSpanContextValue, TRValue
@@ -460,8 +460,8 @@ class Collection:
)
return self.backend.search_cards(search=query, order=mode)
- def find_notes(self, query: str) -> Sequence[int]:
- return self.backend.search_notes(query)
+ def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]:
+ return self.backend.search_notes(self.build_search_string(*terms))
def find_and_replace(
self,
@@ -474,8 +474,39 @@ class Collection:
) -> int:
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
+ # returns array of ("dupestr", [nids])
def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]:
- return anki.find.findDupes(self, fieldName, search)
+ nids = self.findNotes(search, SearchTerm(field_name=fieldName))
+ # go through notes
+ vals: Dict[str, List[int]] = {}
+ dupes = []
+ fields: Dict[int, int] = {}
+
+ def ordForMid(mid):
+ if mid not in fields:
+ model = self.models.get(mid)
+ for c, f in enumerate(model["flds"]):
+ if f["name"].lower() == fieldName.lower():
+ fields[mid] = c
+ break
+ return fields[mid]
+
+ for nid, mid, flds in self.db.all(
+ "select id, mid, flds from notes where id in " + ids2str(nids)
+ ):
+ flds = splitFields(flds)
+ ord = ordForMid(mid)
+ if ord is None:
+ continue
+ val = flds[ord]
+ val = stripHTMLMedia(val)
+ # empty does not count as duplicate
+ if not val:
+ continue
+ vals.setdefault(val, []).append(nid)
+ if len(vals[val]) == 2:
+ dupes.append((val, vals[val]))
+ return dupes
findCards = find_cards
findNotes = find_notes
@@ -484,68 +515,35 @@ class Collection:
# Search Strings
##########################################################################
- def search_string(
- self,
- *,
- negate: bool = False,
- concat_by_or: bool = False,
- searches: Optional[List[str]] = None,
- name: Optional["FilterToSearchIn.NamedFilterValue"] = None,
- tag: Optional[str] = None,
- deck: Optional[str] = None,
- note: Optional[str] = None,
- template: Optional[int] = None,
- dupe: Optional[Tuple[int, str]] = None,
- forgot_in_days: Optional[int] = None,
- added_in_days: Optional[int] = None,
- due_in_days: Optional[int] = None,
- nids: Optional[List[int]] = None,
- field_name: Optional[str] = None,
+ def build_search_string(
+ self, *terms: Union[str, SearchTerm], negate=False, match_any=False
) -> str:
"""Helper function for the backend's search string operations.
- Pass search strings as 'search_strings' to normalize.
- Pass multiple to concatenate (defaults to 'and').
+ Pass terms as strings to normalize.
+ Pass fields of backend.proto/FilterToSearchIn as valid SearchTerms.
+ Pass multiple terms to concatenate (defaults to 'and', 'or' when 'match_any=True').
Pass 'negate=True' to negate the end result.
May raise InvalidInput.
"""
- def append_filter(filter_in):
- filters.append(self.backend.filter_to_search(filter_in))
-
- if name:
- append_filter(FilterToSearchIn(name=name))
- if tag:
- append_filter(FilterToSearchIn(tag=tag))
- if deck:
- append_filter(FilterToSearchIn(deck=deck))
- if note:
- append_filter(FilterToSearchIn(note=note))
- if template:
- append_filter(FilterToSearchIn(template=template))
- if dupe:
- dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=dupe[0]), text=dupe[1])
- append_filter(FilterToSearchIn(dupe=dupe_in))
- if forgot_in_days:
- append_filter(FilterToSearchIn(forgot_in_days=forgot_in_days))
- if added_in_days:
- append_filter(FilterToSearchIn(added_in_days=added_in_days))
- if due_in_days:
- append_filter(FilterToSearchIn(due_in_days=due_in_days))
- if nids:
- append_filter(FilterToSearchIn(nids=NoteIDs(nids=nids)))
- if field_name:
- append_filter(FilterToSearchIn(field_name=field_name))
- if concat_by_or:
+ searches = []
+ for term in terms:
+ if isinstance(term, SearchTerm):
+ term = self.backend.filter_to_search(term)
+ searches.append(term)
+ if match_any:
sep = ConcatSeparator.OR
else:
sep = ConcatSeparator.AND
- search_string = self.backend.concatenate_searches(sep=sep, searches=filters)
+ search_string = self.backend.concatenate_searches(sep=sep, searches=searches)
if negate:
search_string = self.backend.negate_search(search_string)
return search_string
def replace_search_term(self, search: str, replacement: str) -> str:
+ """Wrapper for the according backend function."""
+
return self.backend.replace_search_term(search=search, replacement=replacement)
# Config
@@ -788,5 +786,18 @@ table.review-log {{ {revlog_style} }}
)
+def dupe_search_term(mid: int, text: str) -> SearchTerm:
+ """Helper function for building a DupeIn message."""
+
+ dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text)
+ return SearchTerm(dupe=dupe_in)
+
+
+def nid_search_term(nids: List[int]) -> SearchTerm:
+ """Helper function for building a NoteIDs message."""
+
+ return SearchTerm(nids=NoteIDs(nids=nids))
+
+
# legacy name
_Collection = Collection
diff --git a/pylib/anki/find.py b/pylib/anki/find.py
index c7bf14cdb..ff83d5e2e 100644
--- a/pylib/anki/find.py
+++ b/pylib/anki/find.py
@@ -3,10 +3,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Optional, Set, Tuple
+from typing import TYPE_CHECKING, Optional, Set
from anki.hooks import *
-from anki.utils import ids2str, splitFields, stripHTMLMedia
if TYPE_CHECKING:
from anki.collection import Collection
@@ -64,41 +63,3 @@ def fieldNames(col, downcase=True) -> List:
if name not in fields: # slower w/o
fields.add(name)
return list(fields)
-
-
-# returns array of ("dupestr", [nids])
-def findDupes(
- col: Collection, fieldName: str, search: str = ""
-) -> List[Tuple[Any, List]]:
- # limit search to notes with applicable field name
- search = col.search_string(searches=[search], field_name=fieldName)
- # go through notes
- vals: Dict[str, List[int]] = {}
- dupes = []
- fields: Dict[int, int] = {}
-
- def ordForMid(mid):
- if mid not in fields:
- model = col.models.get(mid)
- for c, f in enumerate(model["flds"]):
- if f["name"].lower() == fieldName.lower():
- fields[mid] = c
- break
- return fields[mid]
-
- for nid, mid, flds in col.db.all(
- "select id, mid, flds from notes where id in " + ids2str(col.findNotes(search))
- ):
- flds = splitFields(flds)
- ord = ordForMid(mid)
- if ord is None:
- continue
- val = flds[ord]
- val = stripHTMLMedia(val)
- # empty does not count as duplicate
- if not val:
- continue
- vals.setdefault(val, []).append(nid)
- if len(vals[val]) == 2:
- dupes.append((val, vals[val]))
- return dupes
diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py
index d4b16fe71..90ed6a815 100644
--- a/pylib/anki/rsbackend.py
+++ b/pylib/anki/rsbackend.py
@@ -47,8 +47,8 @@ TagTreeNode = pb.TagTreeNode
NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode
StockNoteType = pb.StockNoteType
-FilterToSearchIn = pb.FilterToSearchIn
-NamedFilter = pb.FilterToSearchIn.NamedFilter
+SearchTerm = pb.FilterToSearchIn
+Flag = pb.FilterToSearchIn.Flag
DupeIn = pb.FilterToSearchIn.DupeIn
NoteIDs = pb.NoteIDs
BackendNoteTypeID = pb.NoteTypeID
diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py
index 5ced38aa2..c8e969568 100644
--- a/pylib/anki/tags.py
+++ b/pylib/anki/tags.py
@@ -16,7 +16,7 @@ import re
from typing import Collection, List, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import
-from anki.rsbackend import FilterToSearchIn
+from anki.collection import SearchTerm
from anki.utils import ids2str
@@ -87,8 +87,7 @@ class TagManager:
def rename_tag(self, old: str, new: str) -> int:
"Rename provided tag, returning number of changed notes."
- search = self.col.backend.filter_to_search(FilterToSearchIn(tag=old))
- nids = self.col.find_notes(search)
+ nids = self.col.find_notes(SearchTerm(tag=old))
if not nids:
return 0
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)
diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py
index 3cfdde261..7de8b3f24 100644
--- a/qt/aqt/addcards.py
+++ b/qt/aqt/addcards.py
@@ -7,6 +7,7 @@ import aqt.deckchooser
import aqt.editor
import aqt.forms
import aqt.modelchooser
+from anki.collection import nid_search_term
from anki.consts import MODEL_CLOZE
from anki.notes import Note
from anki.utils import htmlToTextLine, isMac
@@ -144,7 +145,7 @@ class AddCards(QDialog):
def onHistory(self) -> None:
m = QMenu(self)
for nid in self.history:
- if self.mw.col.findNotes(self.mw.col.search_string(nids=[nid])):
+ if self.mw.col.findNotes(nid_search_term([nid])):
note = self.mw.col.getNote(nid)
fields = note.fields
txt = htmlToTextLine(", ".join(fields))
@@ -161,7 +162,7 @@ class AddCards(QDialog):
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid):
- self.mw.browser_search(nids=[nid])
+ self.mw.browser_search(nid_search_term([nid]))
def addNote(self, note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId()
diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py
index 317652c5a..a91834d01 100644
--- a/qt/aqt/browser.py
+++ b/qt/aqt/browser.py
@@ -13,7 +13,7 @@ from typing import List, Optional, Sequence, Tuple, cast
import aqt
import aqt.forms
from anki.cards import Card
-from anki.collection import Collection, InvalidInput, NamedFilter
+from anki.collection import Collection, Flag, InvalidInput, SearchTerm, nid_search_term
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.models import NoteType
@@ -612,7 +612,9 @@ class Browser(QMainWindow):
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
- self._searchPromptFilter = self.col.search_string(name=NamedFilter.CURRENT_DECK)
+ self._searchPromptFilter = self.col.build_search_string(
+ SearchTerm(current_deck=True)
+ )
self.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
)
@@ -659,7 +661,7 @@ class Browser(QMainWindow):
c = self.card = self.mw.reviewer.card
nid = c and c.nid or 0
if nid:
- search = self.col.search_string(nids=[nid])
+ search = self.col.build_search_string(nid_search_term([nid]))
search = gui_hooks.default_search(search, c)
self.model.search(search)
self.focusCid(c.id)
@@ -671,7 +673,7 @@ class Browser(QMainWindow):
self._onRowChanged(None, None)
def normalize_search(self, search: str) -> str:
- normed = self.col.search_string(searches=[search])
+ normed = self.col.build_search_string(search)
self._lastSearchTxt = normed
self.form.searchEdit.lineEdit().setText(normed)
return normed
@@ -951,23 +953,21 @@ QTableView {{ gridline-color: {grid} }}
ml.popupOver(self.form.filter)
- def update_search(self, *terms: str):
+ def update_search(self, *terms: Union[str, SearchTerm]):
"Modify the current search string based on modified keys, then refresh."
try:
- search = self.col.search_string(searches=list(terms))
+ search = self.col.build_search_string(*terms)
mods = self.mw.app.keyboardModifiers()
if mods & Qt.AltModifier:
- search = self.col.search_string(negate=True, searches=[search])
+ search = self.col.build_search_string(search, negate=True)
cur = str(self.form.searchEdit.lineEdit().text())
if cur != self._searchPrompt:
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
search = self.col.replace_search_term(cur, search)
elif mods & Qt.ControlModifier:
- search = self.col.search_string(searches=[cur, search])
+ search = self.col.build_search_string(cur, search)
elif mods & Qt.ShiftModifier:
- search = self.col.search_string(
- concat_by_or=True, searches=[cur, search]
- )
+ search = self.col.build_search_string(cur, search, match_any=True)
except InvalidInput as e:
show_invalid_search_error(e)
else:
@@ -993,9 +993,9 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild(
self._simpleFilters(
(
- (tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY),
- (tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY),
- (tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY),
+ (tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)),
+ (tr(TR.BROWSING_STUDIED_TODAY), SearchTerm(studied_today=True)),
+ (tr(TR.BROWSING_AGAIN_TODAY), SearchTerm(forgot_in_days=1)),
)
)
)
@@ -1006,20 +1006,20 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild(
self._simpleFilters(
(
- (tr(TR.ACTIONS_NEW), NamedFilter.NEW),
- (tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN),
- (tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW),
- (tr(TR.FILTERING_IS_DUE), NamedFilter.DUE),
+ (tr(TR.ACTIONS_NEW), SearchTerm(new=True)),
+ (tr(TR.SCHEDULING_LEARNING), SearchTerm(learn=True)),
+ (tr(TR.SCHEDULING_REVIEW), SearchTerm(review=True)),
+ (tr(TR.FILTERING_IS_DUE), SearchTerm(due=True)),
None,
- (tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED),
- (tr(TR.BROWSING_BURIED), NamedFilter.BURIED),
+ (tr(TR.BROWSING_SUSPENDED), SearchTerm(suspended=True)),
+ (tr(TR.BROWSING_BURIED), SearchTerm(buried=True)),
None,
- (tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG),
- (tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG),
- (tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG),
- (tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG),
- (tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG),
- (tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG),
+ (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=Flag.RED)),
+ (tr(TR.ACTIONS_ORANGE_FLAG), SearchTerm(flag=Flag.ORANGE)),
+ (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=Flag.GREEN)),
+ (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=Flag.BLUE)),
+ (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=Flag.WITHOUT)),
+ (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=Flag.ANY)),
)
)
)
@@ -1045,9 +1045,7 @@ QTableView {{ gridline-color: {grid} }}
def _onSaveFilter(self) -> None:
try:
- filt = self.col.search_string(
- searches=[self.form.searchEdit.lineEdit().text()]
- )
+ filt = self.col.build_search_string(self.form.searchEdit.lineEdit().text())
except InvalidInput as e:
show_invalid_search_error(e)
else:
@@ -1088,12 +1086,12 @@ QTableView {{ gridline-color: {grid} }}
def _currentFilterIsSaved(self) -> Optional[str]:
filt = self.form.searchEdit.lineEdit().text()
try:
- filt = self.col.search_string(searches=[filt])
+ filt = self.col.build_search_string(filt)
except InvalidInput:
pass
for k, v in self.col.get_config("savedFilters").items():
try:
- v = self.col.search_string(searches=[v])
+ v = self.col.build_search_string(v)
except InvalidInput:
pass
if filt == v:
@@ -1494,7 +1492,7 @@ where id in %s"""
tv = self.form.tableView
tv.selectionModel().clear()
- search = self.col.search_string(nids=nids)
+ search = self.col.build_search_string(nid_search_term(nids))
self.search_for(search)
tv.selectAll()
@@ -1703,7 +1701,7 @@ where id in %s"""
t += (
"""
%s: %s"""
% (
- self.col.search_string(nids=nids).replace('"', """),
+ html.escape(self.col.build_search_string(nid_search_term(nids))),
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
html.escape(val),
)
diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py
index ca5c5c3c6..e65f4040b 100644
--- a/qt/aqt/customstudy.py
+++ b/qt/aqt/customstudy.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt
-from anki.collection import NamedFilter
+from anki.collection import SearchTerm
from anki.consts import *
from aqt.qt import *
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
@@ -160,29 +160,33 @@ class CustomStudy(QDialog):
dyn = self.mw.col.decks.get(did)
# and then set various options
if i == RADIO_FORGOT:
- search = self.mw.col.search_string(forgot_in_days=spin)
+ search = self.mw.col.build_search_string(SearchTerm(forgot_in_days=spin))
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
dyn["resched"] = False
elif i == RADIO_AHEAD:
- search = self.mw.col.search_string(due_in_days=spin)
+ search = self.mw.col.build_search_string(SearchTerm(due_in_days=spin))
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE]
dyn["resched"] = True
elif i == RADIO_PREVIEW:
- search = self.mw.col.search_string(name=NamedFilter.NEW, added_in_days=spin)
+ search = self.mw.col.build_search_string(
+ SearchTerm(new=True), SearchTerm(added_in_days=spin)
+ )
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST]
dyn["resched"] = False
elif i == RADIO_CRAM:
type = f.cardType.currentRow()
if type == TYPE_NEW:
- terms = self.mw.col.search_string(name=NamedFilter.NEW)
+ terms = self.mw.col.build_search_string(SearchTerm(new=True))
ord = DYN_ADDED
dyn["resched"] = True
elif type == TYPE_DUE:
- terms = self.mw.col.search_string(name=NamedFilter.DUE)
+ terms = self.mw.col.build_search_string(SearchTerm(due=True))
ord = DYN_DUE
dyn["resched"] = True
elif type == TYPE_REVIEW:
- terms = self.mw.col.search_string(negate=True, name=NamedFilter.NEW)
+ terms = self.mw.col.build_search_string(
+ SearchTerm(new=True), negate=True
+ )
ord = DYN_RANDOM
dyn["resched"] = True
else:
@@ -191,8 +195,8 @@ class CustomStudy(QDialog):
dyn["resched"] = False
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
# add deck limit
- dyn["terms"][0][0] = self.mw.col.search_string(
- deck=self.deck["name"], searches=[dyn["terms"][0][0]]
+ dyn["terms"][0][0] = self.mw.col.build_search_string(
+ dyn["terms"][0][0], SearchTerm(deck=self.deck["name"])
)
self.mw.col.decks.save(dyn)
# generate cards
diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py
index 9ae506431..1a0242835 100644
--- a/qt/aqt/dyndeckconf.py
+++ b/qt/aqt/dyndeckconf.py
@@ -4,7 +4,7 @@
from typing import List, Optional
import aqt
-from anki.collection import InvalidInput, NamedFilter
+from anki.collection import InvalidInput, SearchTerm
from anki.lang import without_unicode_isolation
from aqt.qt import *
from aqt.utils import (
@@ -47,11 +47,9 @@ class DeckConf(QDialog):
self.initialSetup()
self.loadConf()
if search:
- search = self.mw.col.search_string(searches=[search], name=NamedFilter.DUE)
+ search = self.mw.col.build_search_string(search, SearchTerm(due=True))
self.form.search.setText(search)
- search_2 = self.mw.col.search_string(
- searches=[search], name=NamedFilter.NEW
- )
+ search_2 = self.mw.col.build_search_string(search, SearchTerm(new=True))
self.form.search_2.setText(search_2)
self.form.search.selectAll()
@@ -123,11 +121,11 @@ class DeckConf(QDialog):
else:
d["delays"] = None
- search = self.mw.col.search_string(searches=[f.search.text()])
+ search = self.mw.col.build_search_string(f.search.text())
terms = [[search, f.limit.value(), f.order.currentIndex()]]
if f.secondFilter.isChecked():
- search_2 = self.mw.col.search_string(searches=[f.search_2.text()])
+ search_2 = self.mw.col.build_search_string(f.search_2.text())
terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()])
d["terms"] = terms
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index cff937d44..9899390c5 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -21,6 +21,7 @@ from bs4 import BeautifulSoup
import aqt
import aqt.sound
from anki.cards import Card
+from anki.collection import dupe_search_term
from anki.hooks import runFilter
from anki.httpclient import HttpClient
from anki.notes import Note
@@ -539,7 +540,9 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self):
- self.mw.browser_search(dupe=(self.note.model()["id"], self.note.fields[0]))
+ self.mw.browser_search(
+ dupe_search_term(self.note.model()["id"], self.note.fields[0])
+ )
def fieldsAreBlank(self, previousNote=None):
if not self.note:
diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py
index 49df85598..e399ce1da 100644
--- a/qt/aqt/emptycards.py
+++ b/qt/aqt/emptycards.py
@@ -66,7 +66,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button.clicked.connect(self._on_delete)
def _on_note_link_clicked(self, link):
- self.mw.browser_search(searches=[link])
+ self.mw.browser_search(link)
def _on_delete(self):
self.mw.progress.start()
diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index b0e46f6c3..c98973da9 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -26,7 +26,7 @@ import aqt.stats
import aqt.toolbar
import aqt.webview
from anki import hooks
-from anki.collection import Collection
+from anki.collection import Collection, SearchTerm
from anki.decks import Deck
from anki.hooks import runHook
from anki.lang import without_unicode_isolation
@@ -1141,7 +1141,7 @@ title="%s" %s>%s""" % (
deck = self.col.decks.current()
if not search:
if not deck["dyn"]:
- search = self.col.search_string(deck=deck["name"])
+ search = self.col.build_search_string(SearchTerm(deck=deck["name"]))
while self.col.decks.id_for_name(
without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n))
):
@@ -1621,10 +1621,10 @@ title="%s" %s>%s""" % (
# Helpers for all windows
##########################################################################
- def browser_search(self, **kwargs) -> None:
- """Wrapper for col.search_string() to look up the result in the browser."""
+ def browser_search(self, *terms: Union[str, SearchTerm]) -> None:
+ """Wrapper for col.build_search_string() to look up the result in the browser."""
- search = self.col.search_string(**kwargs)
+ search = self.col.build_search_string(*terms)
browser = aqt.dialogs.open("Browser", self)
browser.form.searchEdit.lineEdit().setText(search)
browser.onSearchActivated()
diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py
index 86616cec1..3b128975d 100644
--- a/qt/aqt/mediacheck.py
+++ b/qt/aqt/mediacheck.py
@@ -9,6 +9,7 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt
+from anki.collection import nid_search_term
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
from aqt.qt import *
from aqt.utils import (
@@ -145,7 +146,7 @@ class MediaChecker:
if out is not None:
nid, err = out
- self.mw.browser_search(nids=[nid])
+ self.mw.browser_search(nid_search_term([nid]))
showText(err, type="html")
else:
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))
diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py
index 7f0eef43e..088065c95 100644
--- a/qt/aqt/overview.py
+++ b/qt/aqt/overview.py
@@ -7,6 +7,7 @@ from dataclasses import dataclass
from typing import Optional
import aqt
+from anki.collection import SearchTerm
from aqt import gui_hooks
from aqt.sound import av_player
from aqt.toolbar import BottomBar
@@ -72,7 +73,7 @@ class Overview:
self.mw.onDeckConf()
elif url == "cram":
deck = self.mw.col.decks.current()["name"]
- self.mw.onCram(self.mw.col.search_string(deck=deck))
+ self.mw.onCram(self.mw.col.build_search_string(SearchTerm(deck=deck)))
elif url == "refresh":
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py
index f9732241c..d90c2804d 100644
--- a/qt/aqt/sidebar.py
+++ b/qt/aqt/sidebar.py
@@ -9,10 +9,7 @@ from enum import Enum
from typing import Iterable, List, Optional
import aqt
-from anki.collection import ( # pylint: disable=unused-import
- FilterToSearchIn,
- NamedFilter,
-)
+from anki.collection import SearchTerm
from anki.errors import DeckRenameError
from anki.rsbackend import DeckTreeNode, TagTreeNode
from aqt import gui_hooks
@@ -294,14 +291,14 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg",
- self._named_filter(NamedFilter.WHOLE_COLLECTION),
+ self._filter_func(SearchTerm(whole_collection=True)),
item_type=SidebarItemType.COLLECTION,
)
root.addChild(item)
item = SidebarItem(
tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg",
- self._named_filter(NamedFilter.CURRENT_DECK),
+ self._filter_func(SearchTerm(current_deck=True)),
item_type=SidebarItemType.CURRENT_DECK,
)
root.addChild(item)
@@ -313,7 +310,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
name,
":/icons/heart.svg",
- self._saved_filter(filt),
+ self._filter_func(filt),
item_type=SidebarItemType.FILTER,
)
root.addChild(item)
@@ -333,7 +330,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
":/icons/tag.svg",
- self._tag_filter(head + node.name),
+ self._filter_func(SearchTerm(tag=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.TAG,
@@ -358,7 +355,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
":/icons/deck.svg",
- self._deck_filter(head + node.name),
+ self._filter_func(SearchTerm(deck=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.DECK,
@@ -377,7 +374,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
nt["name"],
":/icons/notetype.svg",
- self._note_filter(nt["name"]),
+ self._filter_func(SearchTerm(note=nt["name"])),
item_type=SidebarItemType.NOTETYPE,
id=nt["id"],
)
@@ -386,32 +383,17 @@ class SidebarTreeView(QTreeView):
child = SidebarItem(
tmpl["name"],
":/icons/notetype.svg",
- self._template_filter(nt["name"], c),
+ self._filter_func(
+ SearchTerm(note=nt["name"]), SearchTerm(template=c)
+ ),
item_type=SidebarItemType.TEMPLATE,
)
item.addChild(child)
root.addChild(item)
- def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
- return lambda: self.browser.update_search(self.col.search_string(name=name))
-
- def _tag_filter(self, tag: str) -> Callable:
- return lambda: self.browser.update_search(self.col.search_string(tag=tag))
-
- def _deck_filter(self, deck: str) -> Callable:
- return lambda: self.browser.update_search(self.col.search_string(deck=deck))
-
- def _note_filter(self, note: str) -> Callable:
- return lambda: self.browser.update_search(self.col.search_string(note=note))
-
- def _template_filter(self, note: str, template: int) -> Callable:
- return lambda: self.browser.update_search(
- self.col.search_string(note=note), self.col.search_string(template=template)
- )
-
- def _saved_filter(self, saved: str) -> Callable:
- return lambda: self.browser.update_search(saved)
+ def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
+ return lambda: self.browser.update_search(self.col.build_search_string(*terms))
# Context menu actions
###########################
diff --git a/rslib/backend.proto b/rslib/backend.proto
index fed42f957..44b028845 100644
--- a/rslib/backend.proto
+++ b/rslib/backend.proto
@@ -765,41 +765,39 @@ message BuiltinSearchOrder {
}
message FilterToSearchIn {
- enum NamedFilter {
- WHOLE_COLLECTION = 0;
- CURRENT_DECK = 1;
- ADDED_TODAY = 2;
- STUDIED_TODAY = 3;
- AGAIN_TODAY = 4;
- NEW = 5;
- LEARN = 6;
- REVIEW = 7;
- DUE = 8;
- SUSPENDED = 9;
- BURIED = 10;
- RED_FLAG = 11;
- ORANGE_FLAG = 12;
- GREEN_FLAG = 13;
- BLUE_FLAG = 14;
- NO_FLAG = 15;
- ANY_FLAG = 16;
- }
message DupeIn {
NoteTypeID mid = 1;
string text = 2;
}
+ enum Flag {
+ WITHOUT = 0;
+ ANY = 1;
+ RED = 2;
+ ORANGE = 3;
+ GREEN = 4;
+ BLUE = 5;
+ }
oneof filter {
- NamedFilter name = 1;
- string tag = 2;
- string deck = 3;
- string note = 4;
- uint32 template = 5;
+ string tag = 1;
+ string deck = 2;
+ string note = 3;
+ uint32 template = 4;
+ NoteIDs nids = 5;
DupeIn dupe = 6;
- uint32 forgot_in_days = 7;
- uint32 added_in_days = 8;
- int32 due_in_days = 9;
- NoteIDs nids = 10;
- string field_name = 11;
+ string field_name = 7;
+ uint32 forgot_in_days = 8;
+ uint32 added_in_days = 9;
+ int32 due_in_days = 10;
+ bool whole_collection = 11;
+ bool current_deck = 12;
+ bool studied_today = 13;
+ bool new = 14;
+ bool learn = 15;
+ bool review = 16;
+ bool due = 17;
+ bool suspended = 18;
+ bool buried = 19;
+ Flag flag = 20;
}
}
diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs
index 5a6f6aba2..8d6b19dc7 100644
--- a/rslib/src/backend/mod.rs
+++ b/rslib/src/backend/mod.rs
@@ -293,38 +293,8 @@ impl From for DeckConfID {
impl From for Node<'_> {
fn from(msg: pb::FilterToSearchIn) -> Self {
use pb::filter_to_search_in::Filter;
- use pb::filter_to_search_in::NamedFilter;
- match msg
- .filter
- .unwrap_or(Filter::Name(NamedFilter::WholeCollection as i32))
- {
- Filter::Name(name) => {
- match NamedFilter::from_i32(name).unwrap_or(NamedFilter::WholeCollection) {
- NamedFilter::WholeCollection => Node::Search(SearchNode::WholeCollection),
- NamedFilter::CurrentDeck => Node::Search(SearchNode::Deck("current".into())),
- NamedFilter::AddedToday => Node::Search(SearchNode::AddedInDays(1)),
- NamedFilter::StudiedToday => Node::Search(SearchNode::Rated {
- days: 1,
- ease: EaseKind::AnyAnswerButton,
- }),
- NamedFilter::AgainToday => Node::Search(SearchNode::Rated {
- days: 1,
- ease: EaseKind::AnswerButton(1),
- }),
- NamedFilter::New => Node::Search(SearchNode::State(StateKind::New)),
- NamedFilter::Learn => Node::Search(SearchNode::State(StateKind::Learning)),
- NamedFilter::Review => Node::Search(SearchNode::State(StateKind::Review)),
- NamedFilter::Due => Node::Search(SearchNode::State(StateKind::Due)),
- NamedFilter::Suspended => Node::Search(SearchNode::State(StateKind::Suspended)),
- NamedFilter::Buried => Node::Search(SearchNode::State(StateKind::Buried)),
- NamedFilter::RedFlag => Node::Search(SearchNode::Flag(1)),
- NamedFilter::OrangeFlag => Node::Search(SearchNode::Flag(2)),
- NamedFilter::GreenFlag => Node::Search(SearchNode::Flag(3)),
- NamedFilter::BlueFlag => Node::Search(SearchNode::Flag(4)),
- NamedFilter::NoFlag => Node::Search(SearchNode::Flag(0)),
- NamedFilter::AnyFlag => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))),
- }
- }
+ use pb::filter_to_search_in::Flag;
+ match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
Filter::Tag(s) => Node::Search(SearchNode::Tag(
escape_anki_wildcards(&s).into_owned().into(),
)),
@@ -337,10 +307,16 @@ impl From for Node<'_> {
Filter::Template(u) => {
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
}
+ Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())),
Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates {
note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(),
text: dupe.text.into(),
}),
+ Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
+ field: escape_anki_wildcards(&s).into_owned().into(),
+ text: "*".to_string().into(),
+ is_re: false,
+ }),
Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated {
days: u,
ease: EaseKind::AnswerButton(1),
@@ -350,12 +326,26 @@ impl From for Node<'_> {
operator: "<=".to_string(),
kind: PropertyKind::Due(i),
}),
- Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())),
- Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
- field: escape_anki_wildcards(&s).into_owned().into(),
- text: "*".to_string().into(),
- is_re: false,
+ Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection),
+ Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())),
+ Filter::StudiedToday(_) => Node::Search(SearchNode::Rated {
+ days: 1,
+ ease: EaseKind::AnyAnswerButton,
}),
+ Filter::New(_) => Node::Search(SearchNode::State(StateKind::New)),
+ Filter::Learn(_) => Node::Search(SearchNode::State(StateKind::Learning)),
+ Filter::Review(_) => Node::Search(SearchNode::State(StateKind::Review)),
+ Filter::Due(_) => Node::Search(SearchNode::State(StateKind::Due)),
+ Filter::Suspended(_) => Node::Search(SearchNode::State(StateKind::Suspended)),
+ Filter::Buried(_) => Node::Search(SearchNode::State(StateKind::Buried)),
+ Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) {
+ Flag::Without => Node::Search(SearchNode::Flag(0)),
+ Flag::Any => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))),
+ Flag::Red => Node::Search(SearchNode::Flag(1)),
+ Flag::Orange => Node::Search(SearchNode::Flag(2)),
+ Flag::Green => Node::Search(SearchNode::Flag(3)),
+ Flag::Blue => Node::Search(SearchNode::Flag(4)),
+ },
}
}
}