parent
349bd9d681
commit
c299e271e8
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 += (
|
||||
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
|
||||
% (
|
||||
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),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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</button>""" % (
|
||||
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</button>""" % (
|
||||
# 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()
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
###########################
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,38 +293,8 @@ impl From<pb::DeckConfigId> for DeckConfID {
|
||||
impl From<pb::FilterToSearchIn> 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<pb::FilterToSearchIn> 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<pb::FilterToSearchIn> 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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user