Merge branch 'more-backend-search' into main
This commit is contained in:
commit
cb805cf355
@ -83,6 +83,7 @@ browsing-reposition = Reposition...
|
||||
browsing-reposition-new-cards = Reposition New Cards
|
||||
browsing-reschedule = Reschedule
|
||||
browsing-save-current-filter = Save Current Filter...
|
||||
browsing-search-bar-hint = Type here and press Enter to search for cards and notes.
|
||||
browsing-search-in = Search in:
|
||||
browsing-search-within-formatting-slow = Search within formatting (slow)
|
||||
browsing-shift-position-of-existing-cards = Shift position of existing cards
|
||||
@ -101,7 +102,6 @@ browsing-today = Today
|
||||
browsing-toggle-mark = Toggle Mark
|
||||
browsing-toggle-suspend = Toggle Suspend
|
||||
browsing-treat-input-as-regular-expression = Treat input as regular expression
|
||||
browsing-type-here-to-search = <type here to search; hit enter to show current deck>
|
||||
browsing-whole-collection = Whole Collection
|
||||
browsing-you-must-have-at-least-one = You must have at least one column.
|
||||
browsing-group =
|
||||
|
@ -17,6 +17,7 @@ import anki.find
|
||||
import anki.latex # sets up hook
|
||||
import anki.template
|
||||
from anki import hooks
|
||||
from anki.backend_pb2 import SearchTerm
|
||||
from anki.cards import Card
|
||||
from anki.config import ConfigManager
|
||||
from anki.consts import *
|
||||
@ -26,18 +27,22 @@ from anki.errors import AnkiError
|
||||
from anki.media import MediaManager, media_paths_from_col_path
|
||||
from anki.models import ModelManager
|
||||
from anki.notes import Note
|
||||
from anki.rsbackend import (
|
||||
from anki.rsbackend import ( # pylint: disable=unused-import
|
||||
TR,
|
||||
BackendNoteTypeID,
|
||||
ConcatSeparator,
|
||||
DBError,
|
||||
FormatTimeSpanContext,
|
||||
InvalidInput,
|
||||
Progress,
|
||||
RustBackend,
|
||||
from_json_bytes,
|
||||
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
|
||||
|
||||
ConfigBoolKey = pb.ConfigBool.Key # pylint: disable=no-member
|
||||
|
||||
@ -458,8 +463,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,
|
||||
@ -472,13 +477,76 @@ 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
|
||||
findReplace = find_and_replace
|
||||
|
||||
# Search Strings
|
||||
##########################################################################
|
||||
|
||||
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 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.
|
||||
"""
|
||||
|
||||
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=searches)
|
||||
if negate:
|
||||
search_string = self.backend.negate_search(search_string)
|
||||
return search_string
|
||||
|
||||
def replace_search_term(self, search: str, replacement: str) -> str:
|
||||
return self.backend.replace_search_term(search=search, replacement=replacement)
|
||||
|
||||
# Config
|
||||
##########################################################################
|
||||
|
||||
|
@ -6,7 +6,6 @@ from __future__ import annotations
|
||||
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,43 +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
|
||||
if search:
|
||||
search = "(" + search + ") "
|
||||
search += '"%s:*"' % fieldName.replace('"', '"')
|
||||
# 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
|
||||
|
@ -50,9 +50,6 @@ TagTreeNode = pb.TagTreeNode
|
||||
NoteType = pb.NoteType
|
||||
DeckTreeNode = pb.DeckTreeNode
|
||||
StockNoteType = pb.StockNoteType
|
||||
FilterToSearchIn = pb.FilterToSearchIn
|
||||
NamedFilter = pb.FilterToSearchIn.NamedFilter
|
||||
DupeIn = pb.FilterToSearchIn.DupeIn
|
||||
BackendNoteTypeID = pb.NoteTypeID
|
||||
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
|
||||
SyncAuth = pb.SyncAuth
|
||||
|
@ -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)
|
||||
|
@ -5,6 +5,7 @@ ignore = forms,hooks_gen.py
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
|
||||
ignored-classes=SearchTerm
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
@ -7,6 +7,7 @@ import aqt.deckchooser
|
||||
import aqt.editor
|
||||
import aqt.forms
|
||||
import aqt.modelchooser
|
||||
from anki.collection import SearchTerm
|
||||
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("nid:%s" % nid):
|
||||
if self.mw.col.findNotes(SearchTerm(nid=nid)):
|
||||
note = self.mw.col.getNote(nid)
|
||||
fields = note.fields
|
||||
txt = htmlToTextLine(", ".join(fields))
|
||||
@ -161,9 +162,7 @@ class AddCards(QDialog):
|
||||
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
||||
|
||||
def editHistory(self, nid):
|
||||
browser = aqt.dialogs.open("Browser", self.mw)
|
||||
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
|
||||
browser.onSearchActivated()
|
||||
self.mw.browser_search(SearchTerm(nid=nid))
|
||||
|
||||
def addNote(self, note) -> Optional[Note]:
|
||||
note.model()["did"] = self.deckChooser.selectedId()
|
||||
|
@ -13,19 +13,11 @@ from typing import List, Optional, Sequence, Tuple, cast
|
||||
import aqt
|
||||
import aqt.forms
|
||||
from anki.cards import Card
|
||||
from anki.collection import Collection, ConfigBoolKey
|
||||
from anki.collection import Collection, ConfigBoolKey, InvalidInput, SearchTerm
|
||||
from anki.consts import *
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.models import NoteType
|
||||
from anki.notes import Note
|
||||
from anki.rsbackend import (
|
||||
BackendNoteTypeID,
|
||||
ConcatSeparator,
|
||||
DupeIn,
|
||||
FilterToSearchIn,
|
||||
InvalidInput,
|
||||
NamedFilter,
|
||||
)
|
||||
from anki.stats import CardStats
|
||||
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
@ -193,23 +185,18 @@ class DataModel(QAbstractTableModel):
|
||||
def search(self, txt: str) -> None:
|
||||
self.beginReset()
|
||||
self.cards = []
|
||||
exception: Optional[Exception] = None
|
||||
try:
|
||||
ctx = SearchContext(search=txt, browser=self.browser)
|
||||
gui_hooks.browser_will_search(ctx)
|
||||
if ctx.card_ids is None:
|
||||
ctx.search = self.browser.normalize_search(ctx.search)
|
||||
ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order)
|
||||
gui_hooks.browser_did_search(ctx)
|
||||
self.cards = ctx.card_ids
|
||||
except Exception as e:
|
||||
exception = e
|
||||
except Exception as err:
|
||||
raise err
|
||||
finally:
|
||||
self.endReset()
|
||||
|
||||
if exception:
|
||||
show_invalid_search_error(exception)
|
||||
|
||||
def reset(self):
|
||||
self.beginReset()
|
||||
self.endReset()
|
||||
@ -615,16 +602,13 @@ class Browser(QMainWindow):
|
||||
######################################################################
|
||||
|
||||
def setupSearch(self):
|
||||
qconnect(self.form.searchButton.clicked, self.onSearchActivated)
|
||||
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
||||
self.form.searchEdit.setCompleter(None)
|
||||
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
|
||||
self.form.searchEdit.addItems(
|
||||
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
|
||||
self.form.searchEdit.lineEdit().setPlaceholderText(
|
||||
tr(TR.BROWSING_SEARCH_BAR_HINT)
|
||||
)
|
||||
self.search_for("is:current", self._searchPrompt)
|
||||
# then replace text for easily showing the deck
|
||||
self.form.searchEdit.lineEdit().selectAll()
|
||||
self.form.searchEdit.addItems(self.mw.pm.profile["searchHistory"])
|
||||
self.search_for(self.col.build_search_string(SearchTerm(current_deck=True)), "")
|
||||
self.form.searchEdit.setFocus()
|
||||
|
||||
# search triggered by user
|
||||
@ -632,57 +616,48 @@ class Browser(QMainWindow):
|
||||
self.editor.saveNow(self._onSearchActivated)
|
||||
|
||||
def _onSearchActivated(self):
|
||||
# grab search text and normalize
|
||||
prompt = self.form.searchEdit.lineEdit().text()
|
||||
text = self.form.searchEdit.lineEdit().text()
|
||||
try:
|
||||
normed = self.col.build_search_string(text)
|
||||
except InvalidInput as err:
|
||||
show_invalid_search_error(err)
|
||||
else:
|
||||
self.search_for(normed)
|
||||
self.update_history()
|
||||
|
||||
# convert guide text before we save history
|
||||
txt = "deck:current " if prompt == self._searchPrompt else prompt
|
||||
self.update_history(txt)
|
||||
def search_for(self, search: str, prompt: Optional[str] = None):
|
||||
"""Keep track of search string so that we reuse identical search when
|
||||
refreshing, rather than whatever is currently in the search field.
|
||||
Optionally set the search bar to a different text than the actual search.
|
||||
"""
|
||||
|
||||
# keep track of search string so that we reuse identical search when
|
||||
# refreshing, rather than whatever is currently in the search field
|
||||
self.search_for(txt)
|
||||
self._lastSearchTxt = search
|
||||
prompt = search if prompt == None else prompt
|
||||
self.form.searchEdit.lineEdit().setText(prompt)
|
||||
self.search()
|
||||
|
||||
def update_history(self, search: str) -> None:
|
||||
def search(self):
|
||||
"""Search triggered programmatically. Caller must have saved note first."""
|
||||
|
||||
try:
|
||||
self.model.search(self._lastSearchTxt)
|
||||
except Exception as err:
|
||||
show_invalid_search_error(err)
|
||||
if not self.model.cards:
|
||||
# no row change will fire
|
||||
self._onRowChanged(None, None)
|
||||
|
||||
def update_history(self):
|
||||
sh = self.mw.pm.profile["searchHistory"]
|
||||
if search in sh:
|
||||
sh.remove(search)
|
||||
sh.insert(0, search)
|
||||
if self._lastSearchTxt in sh:
|
||||
sh.remove(self._lastSearchTxt)
|
||||
sh.insert(0, self._lastSearchTxt)
|
||||
sh = sh[:30]
|
||||
self.form.searchEdit.clear()
|
||||
self.form.searchEdit.addItems(sh)
|
||||
self.mw.pm.profile["searchHistory"] = sh
|
||||
|
||||
def search_for(self, search: str, prompt: Optional[str] = None) -> None:
|
||||
self._lastSearchTxt = search
|
||||
self.form.searchEdit.lineEdit().setText(prompt or search)
|
||||
self.search()
|
||||
|
||||
# search triggered programmatically. caller must have saved note first.
|
||||
def search(self) -> None:
|
||||
if "is:current" in self._lastSearchTxt:
|
||||
# show current card if there is one
|
||||
c = self.card = self.mw.reviewer.card
|
||||
nid = c and c.nid or 0
|
||||
if nid:
|
||||
search = "nid:%d" % nid
|
||||
search = gui_hooks.default_search(search, c)
|
||||
self.model.search(search)
|
||||
self.focusCid(c.id)
|
||||
else:
|
||||
self.model.search(self._lastSearchTxt)
|
||||
|
||||
if not self.model.cards:
|
||||
# no row change will fire
|
||||
self._onRowChanged(None, None)
|
||||
|
||||
def normalize_search(self, search: str) -> str:
|
||||
normed = self.col.backend.normalize_search(search)
|
||||
self._lastSearchTxt = normed
|
||||
self.form.searchEdit.lineEdit().setText(normed)
|
||||
return normed
|
||||
|
||||
def updateTitle(self):
|
||||
def updateTitle(self) -> int:
|
||||
selected = len(self.form.tableView.selectionModel().selectedRows())
|
||||
cur = len(self.model.cards)
|
||||
self.setWindowTitle(
|
||||
@ -692,6 +667,21 @@ class Browser(QMainWindow):
|
||||
)
|
||||
return selected
|
||||
|
||||
def show_single_card(self, card: Optional[Card]):
|
||||
"""Try to search for the according note and select the given card."""
|
||||
|
||||
nid: Optional[int] = card and card.nid or 0
|
||||
if nid:
|
||||
|
||||
def on_show_single_card():
|
||||
self.card = card
|
||||
search = self.col.build_search_string(SearchTerm(nid=nid))
|
||||
search = gui_hooks.default_search(search, card)
|
||||
self.search_for(search, "")
|
||||
self.focusCid(card.id)
|
||||
|
||||
self.editor.saveNow(on_show_single_card)
|
||||
|
||||
def onReset(self):
|
||||
self.sidebar.refresh()
|
||||
self.editor.setNote(None)
|
||||
@ -972,33 +962,20 @@ QTableView {{ gridline-color: {grid} }}
|
||||
|
||||
ml.popupOver(self.form.filter)
|
||||
|
||||
def update_search(self, *terms: str):
|
||||
"Modify the current search string based on modified keys, then refresh."
|
||||
def update_search(self, *terms: Union[str, SearchTerm]):
|
||||
"""Modify the current search string based on modified keys, then refresh."""
|
||||
try:
|
||||
search = self.col.backend.concatenate_searches(
|
||||
sep=ConcatSeparator.AND, searches=terms
|
||||
)
|
||||
search = self.col.build_search_string(*terms)
|
||||
mods = self.mw.app.keyboardModifiers()
|
||||
if mods & Qt.AltModifier:
|
||||
search = self.col.backend.negate_search(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.backend.replace_search_term(
|
||||
search=cur, replacement=search
|
||||
)
|
||||
elif mods & Qt.ControlModifier:
|
||||
search = self.col.backend.concatenate_searches(
|
||||
# pylint: disable=no-member
|
||||
sep=ConcatSeparator.AND,
|
||||
searches=[cur, search],
|
||||
)
|
||||
elif mods & Qt.ShiftModifier:
|
||||
search = self.col.backend.concatenate_searches(
|
||||
# pylint: disable=no-member
|
||||
sep=ConcatSeparator.OR,
|
||||
searches=[cur, search],
|
||||
)
|
||||
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
||||
search = self.col.replace_search_term(cur, search)
|
||||
elif mods & Qt.ControlModifier:
|
||||
search = self.col.build_search_string(cur, search)
|
||||
elif mods & Qt.ShiftModifier:
|
||||
search = self.col.build_search_string(cur, search, match_any=True)
|
||||
except InvalidInput as e:
|
||||
show_invalid_search_error(e)
|
||||
else:
|
||||
@ -1016,7 +993,7 @@ QTableView {{ gridline-color: {grid} }}
|
||||
ml.addSeparator()
|
||||
else:
|
||||
label, filter_name = row
|
||||
ml.addItem(label, self.sidebar._named_filter(filter_name))
|
||||
ml.addItem(label, self.sidebar._filter_func(filter_name))
|
||||
return ml
|
||||
|
||||
def _todayFilters(self):
|
||||
@ -1024,9 +1001,19 @@ 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(rated=SearchTerm.Rated(days=1)),
|
||||
),
|
||||
(
|
||||
tr(TR.BROWSING_AGAIN_TODAY),
|
||||
SearchTerm(
|
||||
rated=SearchTerm.Rated(
|
||||
days=1, rating=SearchTerm.RATING_AGAIN
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -1037,20 +1024,41 @@ 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(card_state=SearchTerm.CARD_STATE_NEW),
|
||||
),
|
||||
(
|
||||
tr(TR.SCHEDULING_LEARNING),
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN),
|
||||
),
|
||||
(
|
||||
tr(TR.SCHEDULING_REVIEW),
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW),
|
||||
),
|
||||
(
|
||||
tr(TR.FILTERING_IS_DUE),
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE),
|
||||
),
|
||||
None,
|
||||
(tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED),
|
||||
(tr(TR.BROWSING_BURIED), NamedFilter.BURIED),
|
||||
(
|
||||
tr(TR.BROWSING_SUSPENDED),
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED),
|
||||
),
|
||||
(
|
||||
tr(TR.BROWSING_BURIED),
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED),
|
||||
),
|
||||
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=SearchTerm.FLAG_RED)),
|
||||
(
|
||||
tr(TR.ACTIONS_ORANGE_FLAG),
|
||||
SearchTerm(flag=SearchTerm.FLAG_ORANGE),
|
||||
),
|
||||
(tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.FLAG_GREEN)),
|
||||
(tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.FLAG_BLUE)),
|
||||
(tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.FLAG_NONE)),
|
||||
(tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.FLAG_ANY)),
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -1450,7 +1458,9 @@ where id in %s"""
|
||||
tv = self.form.tableView
|
||||
tv.selectionModel().clear()
|
||||
|
||||
search = "nid:" + ",".join([str(x) for x in nids])
|
||||
search = self.col.build_search_string(
|
||||
SearchTerm(nids=SearchTerm.IdList(ids=nids))
|
||||
)
|
||||
self.search_for(search)
|
||||
|
||||
tv.selectAll()
|
||||
@ -1593,17 +1603,6 @@ where id in %s"""
|
||||
# Edit: finding dupes
|
||||
######################################################################
|
||||
|
||||
# filter called by the editor
|
||||
def search_dupe(self, mid: int, text: str):
|
||||
self.form.searchEdit.lineEdit().setText(
|
||||
self.col.backend.filter_to_search(
|
||||
FilterToSearchIn(
|
||||
dupe=DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.onSearchActivated()
|
||||
|
||||
def onFindDupes(self):
|
||||
self.editor.saveNow(self._onFindDupes)
|
||||
|
||||
@ -1648,7 +1647,12 @@ where id in %s"""
|
||||
|
||||
def duplicatesReport(self, web, fname, search, frm, web_context):
|
||||
self.mw.progress.start()
|
||||
res = self.mw.col.findDupes(fname, search)
|
||||
try:
|
||||
res = self.mw.col.findDupes(fname, search)
|
||||
except InvalidInput as e:
|
||||
self.mw.progress.finish()
|
||||
show_invalid_search_error(e)
|
||||
return
|
||||
if not self._dupesButton:
|
||||
self._dupesButton = b = frm.buttonBox.addButton(
|
||||
tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole
|
||||
@ -1665,7 +1669,11 @@ where id in %s"""
|
||||
t += (
|
||||
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
|
||||
% (
|
||||
"nid:" + ",".join(str(id) for id in nids),
|
||||
html.escape(
|
||||
self.col.build_search_string(
|
||||
SearchTerm(nids=SearchTerm.IdList(ids=nids))
|
||||
)
|
||||
),
|
||||
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
|
||||
html.escape(val),
|
||||
)
|
||||
@ -1761,9 +1769,10 @@ where id in %s"""
|
||||
|
||||
def focusCid(self, cid):
|
||||
try:
|
||||
row = self.model.cards.index(cid)
|
||||
except:
|
||||
row = list(self.model.cards).index(cid)
|
||||
except ValueError:
|
||||
return
|
||||
self.form.tableView.clearSelection()
|
||||
self.form.tableView.selectRow(row)
|
||||
|
||||
|
||||
|
@ -2,6 +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 SearchTerm
|
||||
from anki.consts import *
|
||||
from aqt.qt import *
|
||||
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
|
||||
@ -159,26 +160,42 @@ class CustomStudy(QDialog):
|
||||
dyn = self.mw.col.decks.get(did)
|
||||
# and then set various options
|
||||
if i == RADIO_FORGOT:
|
||||
dyn["terms"][0] = ["rated:%d:1" % spin, DYN_MAX_SIZE, DYN_RANDOM]
|
||||
search = self.mw.col.build_search_string(
|
||||
SearchTerm(
|
||||
rated=SearchTerm.Rated(days=spin, rating=SearchTerm.RATING_AGAIN)
|
||||
)
|
||||
)
|
||||
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
|
||||
dyn["resched"] = False
|
||||
elif i == RADIO_AHEAD:
|
||||
dyn["terms"][0] = ["prop:due<=%d" % spin, DYN_MAX_SIZE, DYN_DUE]
|
||||
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:
|
||||
dyn["terms"][0] = ["is:new added:%s" % spin, DYN_MAX_SIZE, DYN_OLDEST]
|
||||
search = self.mw.col.build_search_string(
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
|
||||
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 = "is:new "
|
||||
terms = self.mw.col.build_search_string(
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
|
||||
)
|
||||
ord = DYN_ADDED
|
||||
dyn["resched"] = True
|
||||
elif type == TYPE_DUE:
|
||||
terms = "is:due "
|
||||
terms = self.mw.col.build_search_string(
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
|
||||
)
|
||||
ord = DYN_DUE
|
||||
dyn["resched"] = True
|
||||
elif type == TYPE_REVIEW:
|
||||
terms = "-is:new "
|
||||
terms = self.mw.col.build_search_string(
|
||||
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), negate=True
|
||||
)
|
||||
ord = DYN_RANDOM
|
||||
dyn["resched"] = True
|
||||
else:
|
||||
@ -187,7 +204,9 @@ class CustomStudy(QDialog):
|
||||
dyn["resched"] = False
|
||||
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
|
||||
# add deck limit
|
||||
dyn["terms"][0][0] = 'deck:"%s" %s ' % (self.deck["name"], 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
|
||||
self.created_custom_study = True
|
||||
|
@ -4,8 +4,8 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import aqt
|
||||
from anki.collection import InvalidInput, SearchTerm
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.rsbackend import InvalidInput
|
||||
from aqt.qt import *
|
||||
from aqt.utils import (
|
||||
TR,
|
||||
@ -47,8 +47,14 @@ class DeckConf(QDialog):
|
||||
self.initialSetup()
|
||||
self.loadConf()
|
||||
if search:
|
||||
self.form.search.setText(search + " is:due")
|
||||
self.form.search_2.setText(search + " is:new")
|
||||
search = self.mw.col.build_search_string(
|
||||
search, SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
|
||||
)
|
||||
self.form.search.setText(search)
|
||||
search_2 = self.mw.col.build_search_string(
|
||||
search, SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
|
||||
)
|
||||
self.form.search_2.setText(search_2)
|
||||
self.form.search.selectAll()
|
||||
|
||||
if self.mw.col.schedVer() == 1:
|
||||
@ -119,11 +125,11 @@ class DeckConf(QDialog):
|
||||
else:
|
||||
d["delays"] = None
|
||||
|
||||
search = self.mw.col.backend.normalize_search(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.backend.normalize_search(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 SearchTerm
|
||||
from anki.consts import MODEL_CLOZE
|
||||
from anki.hooks import runFilter
|
||||
from anki.httpclient import HttpClient
|
||||
@ -537,8 +538,13 @@ class Editor:
|
||||
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
||||
|
||||
def showDupes(self):
|
||||
browser = aqt.dialogs.open("Browser", self.mw)
|
||||
browser.search_dupe(self.note.model()["id"], self.note.fields[0])
|
||||
self.mw.browser_search(
|
||||
SearchTerm(
|
||||
dupe=SearchTerm.Dupe(
|
||||
notetype_id=self.note.model()["id"], first_field=self.note.fields[0]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def fieldsAreBlank(self, previousNote=None):
|
||||
if not self.note:
|
||||
|
@ -66,9 +66,7 @@ class EmptyCardsDialog(QDialog):
|
||||
self._delete_button.clicked.connect(self._on_delete)
|
||||
|
||||
def _on_note_link_clicked(self, link):
|
||||
browser = aqt.dialogs.open("Browser", self.mw)
|
||||
browser.form.searchEdit.lineEdit().setText(link)
|
||||
browser.onSearchActivated()
|
||||
self.mw.browser_search(link)
|
||||
|
||||
def _on_delete(self):
|
||||
self.mw.progress.start()
|
||||
|
@ -107,13 +107,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="searchButton">
|
||||
<property name="text">
|
||||
<string>ACTIONS_SEARCH</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="filter">
|
||||
<property name="text">
|
||||
@ -158,12 +151,12 @@
|
||||
<attribute name="horizontalHeaderCascadingSectionResizes">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>20</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderHighlightSections">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>20</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
@ -223,7 +216,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>750</width>
|
||||
<height>22</height>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
|
@ -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
|
||||
@ -1045,7 +1045,8 @@ title="%s" %s>%s</button>""" % (
|
||||
aqt.dialogs.open("AddCards", self)
|
||||
|
||||
def onBrowse(self) -> None:
|
||||
aqt.dialogs.open("Browser", self)
|
||||
browser = aqt.dialogs.open("Browser", self)
|
||||
browser.show_single_card(self.reviewer.card)
|
||||
|
||||
def onEditCurrent(self):
|
||||
aqt.dialogs.open("EditCurrent", self)
|
||||
@ -1141,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
|
||||
deck = self.col.decks.current()
|
||||
if not search:
|
||||
if not deck["dyn"]:
|
||||
search = 'deck:"%s" ' % 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))
|
||||
):
|
||||
@ -1617,3 +1618,14 @@ title="%s" %s>%s</button>""" % (
|
||||
|
||||
def serverURL(self) -> str:
|
||||
return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
|
||||
|
||||
# Helpers for all windows
|
||||
##########################################################################
|
||||
|
||||
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.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 SearchTerm
|
||||
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
|
||||
from aqt.qt import *
|
||||
from aqt.utils import (
|
||||
@ -145,9 +146,7 @@ class MediaChecker:
|
||||
|
||||
if out is not None:
|
||||
nid, err = out
|
||||
browser = aqt.dialogs.open("Browser", self.mw)
|
||||
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
|
||||
browser.onSearchActivated()
|
||||
self.mw.browser_search(SearchTerm(nid=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
|
||||
@ -71,8 +72,8 @@ class Overview:
|
||||
elif url == "opts":
|
||||
self.mw.onDeckConf()
|
||||
elif url == "cram":
|
||||
deck = self.mw.col.decks.current()
|
||||
self.mw.onCram("'deck:%s'" % deck["name"])
|
||||
deck = self.mw.col.decks.current()["name"]
|
||||
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,15 +9,9 @@ from enum import Enum
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
||||
|
||||
import aqt
|
||||
from anki.collection import ConfigBoolKey
|
||||
from anki.collection import ConfigBoolKey, InvalidInput, SearchTerm
|
||||
from anki.errors import DeckRenameError
|
||||
from anki.rsbackend import (
|
||||
DeckTreeNode,
|
||||
FilterToSearchIn,
|
||||
InvalidInput,
|
||||
NamedFilter,
|
||||
TagTreeNode,
|
||||
)
|
||||
from anki.rsbackend import DeckTreeNode, TagTreeNode
|
||||
from aqt import gui_hooks
|
||||
from aqt.main import ResetReason
|
||||
from aqt.models import Models
|
||||
@ -42,6 +36,7 @@ class SidebarItemType(Enum):
|
||||
COLLECTION = 1
|
||||
CURRENT_DECK = 2
|
||||
SAVED_SEARCH = 3
|
||||
FILTER = 3 # legacy alias for SAVED_SEARCH
|
||||
DECK = 4
|
||||
NOTETYPE = 5
|
||||
TAG = 6
|
||||
@ -466,14 +461,12 @@ class SidebarTreeView(QTreeView):
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_WHOLE_COLLECTION),
|
||||
":/icons/collection.svg",
|
||||
self._named_filter(NamedFilter.WHOLE_COLLECTION),
|
||||
item_type=SidebarItemType.COLLECTION,
|
||||
)
|
||||
root.addChild(item)
|
||||
item = SidebarItem(
|
||||
tr(TR.BROWSING_CURRENT_DECK),
|
||||
":/icons/deck.svg",
|
||||
self._named_filter(NamedFilter.CURRENT_DECK),
|
||||
item_type=SidebarItemType.CURRENT_DECK,
|
||||
)
|
||||
root.addChild(item)
|
||||
@ -499,8 +492,8 @@ class SidebarTreeView(QTreeView):
|
||||
item = SidebarItem(
|
||||
name,
|
||||
icon,
|
||||
self._saved_filter(filt),
|
||||
item_type=SidebarItemType.SAVED_SEARCH,
|
||||
self._filter_func(filt),
|
||||
item_type=SidebarItemType.FILTER,
|
||||
)
|
||||
root.addChild(item)
|
||||
|
||||
@ -519,7 +512,7 @@ class SidebarTreeView(QTreeView):
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
icon,
|
||||
self._tag_filter(head + node.name),
|
||||
self._filter_func(SearchTerm(tag=head + node.name)),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.TAG,
|
||||
@ -551,7 +544,7 @@ class SidebarTreeView(QTreeView):
|
||||
item = SidebarItem(
|
||||
node.name,
|
||||
icon,
|
||||
self._deck_filter(head + node.name),
|
||||
self._filter_func(SearchTerm(deck=head + node.name)),
|
||||
toggle_expand(),
|
||||
not node.collapsed,
|
||||
item_type=SidebarItemType.DECK,
|
||||
@ -584,8 +577,8 @@ class SidebarTreeView(QTreeView):
|
||||
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
|
||||
item = SidebarItem(
|
||||
nt["name"],
|
||||
icon=icon,
|
||||
onClick=self._note_filter(nt["name"]),
|
||||
icon,
|
||||
self._filter_func(SearchTerm(note=nt["name"])),
|
||||
item_type=SidebarItemType.NOTETYPE,
|
||||
id=nt["id"],
|
||||
)
|
||||
@ -594,7 +587,9 @@ class SidebarTreeView(QTreeView):
|
||||
child = SidebarItem(
|
||||
tmpl["name"],
|
||||
icon,
|
||||
onClick=self._template_filter(nt["name"], c),
|
||||
self._filter_func(
|
||||
SearchTerm(note=nt["name"]), SearchTerm(template=c)
|
||||
),
|
||||
item_type=SidebarItemType.TEMPLATE,
|
||||
full_name=nt["name"] + "::" + tmpl["name"],
|
||||
)
|
||||
@ -602,34 +597,8 @@ class SidebarTreeView(QTreeView):
|
||||
|
||||
root.addChild(item)
|
||||
|
||||
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(name=name))
|
||||
)
|
||||
|
||||
def _tag_filter(self, tag: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(tag=tag))
|
||||
)
|
||||
|
||||
def _deck_filter(self, deck: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(deck=deck))
|
||||
)
|
||||
|
||||
def _note_filter(self, note: str) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note))
|
||||
)
|
||||
|
||||
def _template_filter(self, note: str, template: int) -> Callable:
|
||||
return lambda: self.browser.update_search(
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(note=note)),
|
||||
self.col.backend.filter_to_search(FilterToSearchIn(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
|
||||
###########################
|
||||
@ -807,7 +776,7 @@ class SidebarTreeView(QTreeView):
|
||||
|
||||
def save_current_search(self, _item=None) -> None:
|
||||
try:
|
||||
filt = self.col.backend.normalize_search(
|
||||
filt = self.col.build_search_string(
|
||||
self.browser.form.searchEdit.lineEdit().text()
|
||||
)
|
||||
except InvalidInput as e:
|
||||
|
@ -84,7 +84,7 @@ service BackendService {
|
||||
|
||||
// searching
|
||||
|
||||
rpc FilterToSearch(FilterToSearchIn) returns (String);
|
||||
rpc FilterToSearch(SearchTerm) returns (String);
|
||||
rpc NormalizeSearch(String) returns (String);
|
||||
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
||||
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
||||
@ -765,37 +765,58 @@ message BuiltinSearchOrder {
|
||||
bool reverse = 2;
|
||||
}
|
||||
|
||||
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 SearchTerm {
|
||||
message Dupe {
|
||||
int64 notetype_id = 1;
|
||||
string first_field = 2;
|
||||
}
|
||||
message DupeIn {
|
||||
NoteTypeID mid = 1;
|
||||
string text = 2;
|
||||
enum Flag {
|
||||
FLAG_NONE = 0;
|
||||
FLAG_ANY = 1;
|
||||
FLAG_RED = 2;
|
||||
FLAG_ORANGE = 3;
|
||||
FLAG_GREEN = 4;
|
||||
FLAG_BLUE = 5;
|
||||
}
|
||||
enum Rating {
|
||||
RATING_ANY = 0;
|
||||
RATING_AGAIN = 1;
|
||||
RATING_HARD = 2;
|
||||
RATING_GOOD = 3;
|
||||
RATING_EASY = 4;
|
||||
RATING_BY_RESCHEDULE = 5;
|
||||
}
|
||||
message Rated {
|
||||
uint32 days = 1;
|
||||
Rating rating = 2;
|
||||
}
|
||||
enum CardState {
|
||||
CARD_STATE_NEW = 0;
|
||||
CARD_STATE_LEARN = 1;
|
||||
CARD_STATE_REVIEW = 2;
|
||||
CARD_STATE_DUE = 3;
|
||||
CARD_STATE_SUSPENDED = 4;
|
||||
CARD_STATE_BURIED = 5;
|
||||
}
|
||||
message IdList {
|
||||
repeated int64 ids = 1;
|
||||
}
|
||||
oneof filter {
|
||||
NamedFilter name = 1;
|
||||
string tag = 2;
|
||||
string deck = 3;
|
||||
string note = 4;
|
||||
uint32 template = 5;
|
||||
DupeIn dupe = 6;
|
||||
string tag = 1;
|
||||
string deck = 2;
|
||||
string note = 3;
|
||||
uint32 template = 4;
|
||||
int64 nid = 5;
|
||||
Dupe dupe = 6;
|
||||
string field_name = 7;
|
||||
Rated rated = 8;
|
||||
uint32 added_in_days = 9;
|
||||
int32 due_in_days = 10;
|
||||
bool whole_collection = 11;
|
||||
bool current_deck = 12;
|
||||
Flag flag = 13;
|
||||
CardState card_state = 14;
|
||||
IdList nids = 15;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,8 @@ use crate::{
|
||||
sched::timespan::{answer_button_time, time_span},
|
||||
search::{
|
||||
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||
BoolSeparator, EaseKind, Node, SearchNode, SortMode, StateKind, TemplateKind,
|
||||
BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind,
|
||||
TemplateKind,
|
||||
},
|
||||
stats::studied_today,
|
||||
sync::{
|
||||
@ -262,6 +263,16 @@ impl From<pb::NoteId> for NoteID {
|
||||
}
|
||||
}
|
||||
|
||||
impl pb::search_term::IdList {
|
||||
fn into_id_string(self) -> String {
|
||||
self.ids
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::NoteTypeId> for NoteTypeID {
|
||||
fn from(ntid: pb::NoteTypeId) -> Self {
|
||||
NoteTypeID(ntid.ntid)
|
||||
@ -280,41 +291,11 @@ 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)))),
|
||||
}
|
||||
}
|
||||
impl From<pb::SearchTerm> for Node<'_> {
|
||||
fn from(msg: pb::SearchTerm) -> Self {
|
||||
use pb::search_term::Filter;
|
||||
use pb::search_term::Flag;
|
||||
match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
|
||||
Filter::Tag(s) => Node::Search(SearchNode::Tag(
|
||||
escape_anki_wildcards(&s).into_owned().into(),
|
||||
)),
|
||||
@ -327,10 +308,41 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
|
||||
Filter::Template(u) => {
|
||||
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
|
||||
}
|
||||
Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string().into())),
|
||||
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(),
|
||||
note_type_id: dupe.notetype_id.into(),
|
||||
text: dupe.first_field.into(),
|
||||
}),
|
||||
Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
|
||||
field: escape_anki_wildcards(&s).into_owned().into(),
|
||||
text: "*".to_string().into(),
|
||||
is_re: false,
|
||||
}),
|
||||
Filter::Rated(rated) => Node::Search(SearchNode::Rated {
|
||||
days: rated.days,
|
||||
ease: rated.rating().into(),
|
||||
}),
|
||||
Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)),
|
||||
Filter::DueInDays(i) => Node::Search(SearchNode::Property {
|
||||
operator: "<=".to_string(),
|
||||
kind: PropertyKind::Due(i),
|
||||
}),
|
||||
Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection),
|
||||
Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())),
|
||||
Filter::CardState(state) => Node::Search(SearchNode::State(
|
||||
pb::search_term::CardState::from_i32(state)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
)),
|
||||
Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) {
|
||||
Flag::None => 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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -344,6 +356,32 @@ impl From<BoolSeparatorProto> for BoolSeparator {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::search_term::Rating> for RatingKind {
|
||||
fn from(r: pb::search_term::Rating) -> Self {
|
||||
match r {
|
||||
pb::search_term::Rating::Again => RatingKind::AnswerButton(1),
|
||||
pb::search_term::Rating::Hard => RatingKind::AnswerButton(2),
|
||||
pb::search_term::Rating::Good => RatingKind::AnswerButton(3),
|
||||
pb::search_term::Rating::Easy => RatingKind::AnswerButton(4),
|
||||
pb::search_term::Rating::Any => RatingKind::AnyAnswerButton,
|
||||
pb::search_term::Rating::ByReschedule => RatingKind::ManualReschedule,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::search_term::CardState> for StateKind {
|
||||
fn from(k: pb::search_term::CardState) -> Self {
|
||||
match k {
|
||||
pb::search_term::CardState::New => StateKind::New,
|
||||
pb::search_term::CardState::Learn => StateKind::Learning,
|
||||
pb::search_term::CardState::Review => StateKind::Review,
|
||||
pb::search_term::CardState::Due => StateKind::Due,
|
||||
pb::search_term::CardState::Suspended => StateKind::Suspended,
|
||||
pb::search_term::CardState::Buried => StateKind::Buried,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendService for Backend {
|
||||
fn latest_progress(&self, _input: Empty) -> BackendResult<pb::Progress> {
|
||||
let progress = self.progress_state.lock().unwrap().last_progress;
|
||||
@ -466,7 +504,7 @@ impl BackendService for Backend {
|
||||
// searching
|
||||
//-----------------------------------------------
|
||||
|
||||
fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result<pb::String> {
|
||||
fn filter_to_search(&self, input: pb::SearchTerm) -> Result<pb::String> {
|
||||
Ok(write_nodes(&[input.into()]).into())
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ mod sqlwriter;
|
||||
mod writer;
|
||||
|
||||
pub use cards::SortMode;
|
||||
pub use parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind};
|
||||
pub use parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind};
|
||||
pub use writer::{
|
||||
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||
BoolSeparator,
|
||||
|
@ -58,7 +58,7 @@ pub enum SearchNode<'a> {
|
||||
NoteType(Cow<'a, str>),
|
||||
Rated {
|
||||
days: u32,
|
||||
ease: EaseKind,
|
||||
ease: RatingKind,
|
||||
},
|
||||
Tag(Cow<'a, str>),
|
||||
Duplicates {
|
||||
@ -67,7 +67,7 @@ pub enum SearchNode<'a> {
|
||||
},
|
||||
State(StateKind),
|
||||
Flag(u8),
|
||||
NoteIDs(&'a str),
|
||||
NoteIDs(Cow<'a, str>),
|
||||
CardIDs(&'a str),
|
||||
Property {
|
||||
operator: String,
|
||||
@ -87,7 +87,7 @@ pub enum PropertyKind {
|
||||
Lapses(u32),
|
||||
Ease(f32),
|
||||
Position(u32),
|
||||
Rated(i32, EaseKind),
|
||||
Rated(i32, RatingKind),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@ -109,7 +109,7 @@ pub enum TemplateKind<'a> {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum EaseKind {
|
||||
pub enum RatingKind {
|
||||
AnswerButton(u8),
|
||||
AnyAnswerButton,
|
||||
ManualReschedule,
|
||||
@ -318,7 +318,7 @@ fn search_node_for_text_with_argument<'a>(
|
||||
"is" => parse_state(val)?,
|
||||
"did" => parse_did(val)?,
|
||||
"mid" => parse_mid(val)?,
|
||||
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?),
|
||||
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?.into()),
|
||||
"cid" => SearchNode::CardIDs(check_id_list(val, key)?),
|
||||
"re" => SearchNode::Regex(unescape_quotes(val)),
|
||||
"nc" => SearchNode::NoCombining(unescape(val)?),
|
||||
@ -353,7 +353,7 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
||||
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
||||
parse_u32(s, "resched:").map(|days| SearchNode::Rated {
|
||||
days,
|
||||
ease: EaseKind::ManualReschedule,
|
||||
ease: RatingKind::ManualReschedule,
|
||||
})
|
||||
}
|
||||
|
||||
@ -392,7 +392,7 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
||||
"rated" => parse_prop_rated(num, prop_clause)?,
|
||||
"resched" => PropertyKind::Rated(
|
||||
parse_negative_i32(num, prop_clause)?,
|
||||
EaseKind::ManualReschedule,
|
||||
RatingKind::ManualReschedule,
|
||||
),
|
||||
"ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
|
||||
"reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
|
||||
@ -470,9 +470,9 @@ fn parse_i64<'a>(num: &str, context: &'a str) -> ParseResult<'a, i64> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, EaseKind> {
|
||||
fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, RatingKind> {
|
||||
Ok(if let Some(num) = num {
|
||||
EaseKind::AnswerButton(
|
||||
RatingKind::AnswerButton(
|
||||
num.parse()
|
||||
.map_err(|_| ())
|
||||
.and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { Err(()) })
|
||||
@ -487,7 +487,7 @@ fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
EaseKind::AnyAnswerButton
|
||||
RatingKind::AnyAnswerButton
|
||||
})
|
||||
}
|
||||
|
||||
@ -813,7 +813,7 @@ mod test {
|
||||
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
|
||||
assert_eq!(
|
||||
parse("nid:1237123712,2,3")?,
|
||||
vec![Search(NoteIDs("1237123712,2,3"))]
|
||||
vec![Search(NoteIDs("1237123712,2,3".into()))]
|
||||
);
|
||||
assert_eq!(parse("is:due")?, vec![Search(State(StateKind::Due))]);
|
||||
assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]);
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind};
|
||||
use super::parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind};
|
||||
use crate::{
|
||||
card::{CardQueue, CardType},
|
||||
collection::Collection,
|
||||
@ -211,7 +211,7 @@ impl SqlWriter<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_rated(&mut self, op: &str, days: i64, ease: &EaseKind) -> Result<()> {
|
||||
fn write_rated(&mut self, op: &str, days: i64, ease: &RatingKind) -> Result<()> {
|
||||
let today_cutoff = self.col.timing_today()?.next_day_at;
|
||||
let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000;
|
||||
let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000;
|
||||
@ -240,9 +240,9 @@ impl SqlWriter<'_> {
|
||||
.unwrap();
|
||||
|
||||
match ease {
|
||||
EaseKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
|
||||
EaseKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
|
||||
EaseKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
|
||||
RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
|
||||
RatingKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
|
||||
RatingKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
|
@ -5,7 +5,7 @@ use crate::{
|
||||
decks::DeckID as DeckIDType,
|
||||
err::Result,
|
||||
notetype::NoteTypeID as NoteTypeIDType,
|
||||
search::parser::{parse, EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind},
|
||||
search::parser::{parse, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::mem;
|
||||
@ -154,8 +154,8 @@ fn write_template(template: &TemplateKind) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_rated(days: &u32, ease: &EaseKind) -> String {
|
||||
use EaseKind::*;
|
||||
fn write_rated(days: &u32, ease: &RatingKind) -> String {
|
||||
use RatingKind::*;
|
||||
match ease {
|
||||
AnswerButton(n) => format!("\"rated:{}:{}\"", days, n),
|
||||
AnyAnswerButton => format!("\"rated:{}\"", days),
|
||||
@ -196,9 +196,9 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String {
|
||||
Ease(f) => format!("\"prop:ease{}{}\"", operator, f),
|
||||
Position(u) => format!("\"prop:pos{}{}\"", operator, u),
|
||||
Rated(u, ease) => match ease {
|
||||
EaseKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
|
||||
EaseKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
|
||||
EaseKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
|
||||
RatingKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
|
||||
RatingKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
|
||||
RatingKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user