mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Merge branch 'more-backend-search' into main
This commit is contained in:
commit
cb805cf355
23 changed files with 444 additions and 350 deletions
|
@ -83,6 +83,7 @@ browsing-reposition = Reposition...
|
||||||
browsing-reposition-new-cards = Reposition New Cards
|
browsing-reposition-new-cards = Reposition New Cards
|
||||||
browsing-reschedule = Reschedule
|
browsing-reschedule = Reschedule
|
||||||
browsing-save-current-filter = Save Current Filter...
|
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-in = Search in:
|
||||||
browsing-search-within-formatting-slow = Search within formatting (slow)
|
browsing-search-within-formatting-slow = Search within formatting (slow)
|
||||||
browsing-shift-position-of-existing-cards = Shift position of existing cards
|
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-mark = Toggle Mark
|
||||||
browsing-toggle-suspend = Toggle Suspend
|
browsing-toggle-suspend = Toggle Suspend
|
||||||
browsing-treat-input-as-regular-expression = Treat input as regular expression
|
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-whole-collection = Whole Collection
|
||||||
browsing-you-must-have-at-least-one = You must have at least one column.
|
browsing-you-must-have-at-least-one = You must have at least one column.
|
||||||
browsing-group =
|
browsing-group =
|
||||||
|
|
|
@ -17,6 +17,7 @@ import anki.find
|
||||||
import anki.latex # sets up hook
|
import anki.latex # sets up hook
|
||||||
import anki.template
|
import anki.template
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
from anki.backend_pb2 import SearchTerm
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.config import ConfigManager
|
from anki.config import ConfigManager
|
||||||
from anki.consts import *
|
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.media import MediaManager, media_paths_from_col_path
|
||||||
from anki.models import ModelManager
|
from anki.models import ModelManager
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import ( # pylint: disable=unused-import
|
||||||
TR,
|
TR,
|
||||||
|
BackendNoteTypeID,
|
||||||
|
ConcatSeparator,
|
||||||
DBError,
|
DBError,
|
||||||
FormatTimeSpanContext,
|
FormatTimeSpanContext,
|
||||||
|
InvalidInput,
|
||||||
Progress,
|
Progress,
|
||||||
RustBackend,
|
RustBackend,
|
||||||
from_json_bytes,
|
from_json_bytes,
|
||||||
|
pb,
|
||||||
)
|
)
|
||||||
from anki.sched import Scheduler as V1Scheduler
|
from anki.sched import Scheduler as V1Scheduler
|
||||||
from anki.schedv2 import Scheduler as V2Scheduler
|
from anki.schedv2 import Scheduler as V2Scheduler
|
||||||
from anki.tags import TagManager
|
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
|
ConfigBoolKey = pb.ConfigBool.Key # pylint: disable=no-member
|
||||||
|
|
||||||
|
@ -458,8 +463,8 @@ class Collection:
|
||||||
)
|
)
|
||||||
return self.backend.search_cards(search=query, order=mode)
|
return self.backend.search_cards(search=query, order=mode)
|
||||||
|
|
||||||
def find_notes(self, query: str) -> Sequence[int]:
|
def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]:
|
||||||
return self.backend.search_notes(query)
|
return self.backend.search_notes(self.build_search_string(*terms))
|
||||||
|
|
||||||
def find_and_replace(
|
def find_and_replace(
|
||||||
self,
|
self,
|
||||||
|
@ -472,13 +477,76 @@ class Collection:
|
||||||
) -> int:
|
) -> int:
|
||||||
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
|
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]]:
|
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
|
findCards = find_cards
|
||||||
findNotes = find_notes
|
findNotes = find_notes
|
||||||
findReplace = find_and_replace
|
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
|
# Config
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Optional, Set
|
from typing import TYPE_CHECKING, Optional, Set
|
||||||
|
|
||||||
from anki.hooks import *
|
from anki.hooks import *
|
||||||
from anki.utils import ids2str, splitFields, stripHTMLMedia
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
@ -64,43 +63,3 @@ def fieldNames(col, downcase=True) -> List:
|
||||||
if name not in fields: # slower w/o
|
if name not in fields: # slower w/o
|
||||||
fields.add(name)
|
fields.add(name)
|
||||||
return list(fields)
|
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
|
NoteType = pb.NoteType
|
||||||
DeckTreeNode = pb.DeckTreeNode
|
DeckTreeNode = pb.DeckTreeNode
|
||||||
StockNoteType = pb.StockNoteType
|
StockNoteType = pb.StockNoteType
|
||||||
FilterToSearchIn = pb.FilterToSearchIn
|
|
||||||
NamedFilter = pb.FilterToSearchIn.NamedFilter
|
|
||||||
DupeIn = pb.FilterToSearchIn.DupeIn
|
|
||||||
BackendNoteTypeID = pb.NoteTypeID
|
BackendNoteTypeID = pb.NoteTypeID
|
||||||
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
|
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
|
||||||
SyncAuth = pb.SyncAuth
|
SyncAuth = pb.SyncAuth
|
||||||
|
|
|
@ -16,7 +16,7 @@ import re
|
||||||
from typing import Collection, List, Optional, Sequence, Tuple
|
from typing import Collection, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki.rsbackend import FilterToSearchIn
|
from anki.collection import SearchTerm
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,8 +87,7 @@ class TagManager:
|
||||||
|
|
||||||
def rename_tag(self, old: str, new: str) -> int:
|
def rename_tag(self, old: str, new: str) -> int:
|
||||||
"Rename provided tag, returning number of changed notes."
|
"Rename provided tag, returning number of changed notes."
|
||||||
search = self.col.backend.filter_to_search(FilterToSearchIn(tag=old))
|
nids = self.col.find_notes(SearchTerm(tag=old))
|
||||||
nids = self.col.find_notes(search)
|
|
||||||
if not nids:
|
if not nids:
|
||||||
return 0
|
return 0
|
||||||
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)
|
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)
|
||||||
|
|
|
@ -5,6 +5,7 @@ ignore = forms,hooks_gen.py
|
||||||
|
|
||||||
[TYPECHECK]
|
[TYPECHECK]
|
||||||
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
|
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
|
||||||
|
ignored-classes=SearchTerm
|
||||||
|
|
||||||
[REPORTS]
|
[REPORTS]
|
||||||
output-format=colorized
|
output-format=colorized
|
||||||
|
|
|
@ -7,6 +7,7 @@ import aqt.deckchooser
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.modelchooser
|
import aqt.modelchooser
|
||||||
|
from anki.collection import SearchTerm
|
||||||
from anki.consts import MODEL_CLOZE
|
from anki.consts import MODEL_CLOZE
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.utils import htmlToTextLine, isMac
|
from anki.utils import htmlToTextLine, isMac
|
||||||
|
@ -144,7 +145,7 @@ class AddCards(QDialog):
|
||||||
def onHistory(self) -> None:
|
def onHistory(self) -> None:
|
||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
for nid in self.history:
|
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)
|
note = self.mw.col.getNote(nid)
|
||||||
fields = note.fields
|
fields = note.fields
|
||||||
txt = htmlToTextLine(", ".join(fields))
|
txt = htmlToTextLine(", ".join(fields))
|
||||||
|
@ -161,9 +162,7 @@ class AddCards(QDialog):
|
||||||
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
||||||
|
|
||||||
def editHistory(self, nid):
|
def editHistory(self, nid):
|
||||||
browser = aqt.dialogs.open("Browser", self.mw)
|
self.mw.browser_search(SearchTerm(nid=nid))
|
||||||
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
|
|
||||||
browser.onSearchActivated()
|
|
||||||
|
|
||||||
def addNote(self, note) -> Optional[Note]:
|
def addNote(self, note) -> Optional[Note]:
|
||||||
note.model()["did"] = self.deckChooser.selectedId()
|
note.model()["did"] = self.deckChooser.selectedId()
|
||||||
|
|
|
@ -13,19 +13,11 @@ from typing import List, Optional, Sequence, Tuple, cast
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki.cards import Card
|
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.consts import *
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.models import NoteType
|
from anki.models import NoteType
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.rsbackend import (
|
|
||||||
BackendNoteTypeID,
|
|
||||||
ConcatSeparator,
|
|
||||||
DupeIn,
|
|
||||||
FilterToSearchIn,
|
|
||||||
InvalidInput,
|
|
||||||
NamedFilter,
|
|
||||||
)
|
|
||||||
from anki.stats import CardStats
|
from anki.stats import CardStats
|
||||||
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
|
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
@ -193,23 +185,18 @@ class DataModel(QAbstractTableModel):
|
||||||
def search(self, txt: str) -> None:
|
def search(self, txt: str) -> None:
|
||||||
self.beginReset()
|
self.beginReset()
|
||||||
self.cards = []
|
self.cards = []
|
||||||
exception: Optional[Exception] = None
|
|
||||||
try:
|
try:
|
||||||
ctx = SearchContext(search=txt, browser=self.browser)
|
ctx = SearchContext(search=txt, browser=self.browser)
|
||||||
gui_hooks.browser_will_search(ctx)
|
gui_hooks.browser_will_search(ctx)
|
||||||
if ctx.card_ids is None:
|
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)
|
ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order)
|
||||||
gui_hooks.browser_did_search(ctx)
|
gui_hooks.browser_did_search(ctx)
|
||||||
self.cards = ctx.card_ids
|
self.cards = ctx.card_ids
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
exception = e
|
raise err
|
||||||
finally:
|
finally:
|
||||||
self.endReset()
|
self.endReset()
|
||||||
|
|
||||||
if exception:
|
|
||||||
show_invalid_search_error(exception)
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.beginReset()
|
self.beginReset()
|
||||||
self.endReset()
|
self.endReset()
|
||||||
|
@ -615,16 +602,13 @@ class Browser(QMainWindow):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupSearch(self):
|
def setupSearch(self):
|
||||||
qconnect(self.form.searchButton.clicked, self.onSearchActivated)
|
|
||||||
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
||||||
self.form.searchEdit.setCompleter(None)
|
self.form.searchEdit.setCompleter(None)
|
||||||
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
|
self.form.searchEdit.lineEdit().setPlaceholderText(
|
||||||
self.form.searchEdit.addItems(
|
tr(TR.BROWSING_SEARCH_BAR_HINT)
|
||||||
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
|
|
||||||
)
|
)
|
||||||
self.search_for("is:current", self._searchPrompt)
|
self.form.searchEdit.addItems(self.mw.pm.profile["searchHistory"])
|
||||||
# then replace text for easily showing the deck
|
self.search_for(self.col.build_search_string(SearchTerm(current_deck=True)), "")
|
||||||
self.form.searchEdit.lineEdit().selectAll()
|
|
||||||
self.form.searchEdit.setFocus()
|
self.form.searchEdit.setFocus()
|
||||||
|
|
||||||
# search triggered by user
|
# search triggered by user
|
||||||
|
@ -632,57 +616,48 @@ class Browser(QMainWindow):
|
||||||
self.editor.saveNow(self._onSearchActivated)
|
self.editor.saveNow(self._onSearchActivated)
|
||||||
|
|
||||||
def _onSearchActivated(self):
|
def _onSearchActivated(self):
|
||||||
# grab search text and normalize
|
text = self.form.searchEdit.lineEdit().text()
|
||||||
prompt = 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
|
def search_for(self, search: str, prompt: Optional[str] = None):
|
||||||
txt = "deck:current " if prompt == self._searchPrompt else prompt
|
"""Keep track of search string so that we reuse identical search when
|
||||||
self.update_history(txt)
|
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
|
self._lastSearchTxt = search
|
||||||
# refreshing, rather than whatever is currently in the search field
|
prompt = search if prompt == None else prompt
|
||||||
self.search_for(txt)
|
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"]
|
sh = self.mw.pm.profile["searchHistory"]
|
||||||
if search in sh:
|
if self._lastSearchTxt in sh:
|
||||||
sh.remove(search)
|
sh.remove(self._lastSearchTxt)
|
||||||
sh.insert(0, search)
|
sh.insert(0, self._lastSearchTxt)
|
||||||
sh = sh[:30]
|
sh = sh[:30]
|
||||||
self.form.searchEdit.clear()
|
self.form.searchEdit.clear()
|
||||||
self.form.searchEdit.addItems(sh)
|
self.form.searchEdit.addItems(sh)
|
||||||
self.mw.pm.profile["searchHistory"] = sh
|
self.mw.pm.profile["searchHistory"] = sh
|
||||||
|
|
||||||
def search_for(self, search: str, prompt: Optional[str] = None) -> None:
|
def updateTitle(self) -> int:
|
||||||
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):
|
|
||||||
selected = len(self.form.tableView.selectionModel().selectedRows())
|
selected = len(self.form.tableView.selectionModel().selectedRows())
|
||||||
cur = len(self.model.cards)
|
cur = len(self.model.cards)
|
||||||
self.setWindowTitle(
|
self.setWindowTitle(
|
||||||
|
@ -692,6 +667,21 @@ class Browser(QMainWindow):
|
||||||
)
|
)
|
||||||
return selected
|
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):
|
def onReset(self):
|
||||||
self.sidebar.refresh()
|
self.sidebar.refresh()
|
||||||
self.editor.setNote(None)
|
self.editor.setNote(None)
|
||||||
|
@ -972,33 +962,20 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
|
|
||||||
ml.popupOver(self.form.filter)
|
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."
|
"""Modify the current search string based on modified keys, then refresh."""
|
||||||
try:
|
try:
|
||||||
search = self.col.backend.concatenate_searches(
|
search = self.col.build_search_string(*terms)
|
||||||
sep=ConcatSeparator.AND, searches=terms
|
|
||||||
)
|
|
||||||
mods = self.mw.app.keyboardModifiers()
|
mods = self.mw.app.keyboardModifiers()
|
||||||
if mods & Qt.AltModifier:
|
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())
|
cur = str(self.form.searchEdit.lineEdit().text())
|
||||||
if cur != self._searchPrompt:
|
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
||||||
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
search = self.col.replace_search_term(cur, search)
|
||||||
search = self.col.backend.replace_search_term(
|
elif mods & Qt.ControlModifier:
|
||||||
search=cur, replacement=search
|
search = self.col.build_search_string(cur, search)
|
||||||
)
|
elif mods & Qt.ShiftModifier:
|
||||||
elif mods & Qt.ControlModifier:
|
search = self.col.build_search_string(cur, search, match_any=True)
|
||||||
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],
|
|
||||||
)
|
|
||||||
except InvalidInput as e:
|
except InvalidInput as e:
|
||||||
show_invalid_search_error(e)
|
show_invalid_search_error(e)
|
||||||
else:
|
else:
|
||||||
|
@ -1016,7 +993,7 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
ml.addSeparator()
|
ml.addSeparator()
|
||||||
else:
|
else:
|
||||||
label, filter_name = row
|
label, filter_name = row
|
||||||
ml.addItem(label, self.sidebar._named_filter(filter_name))
|
ml.addItem(label, self.sidebar._filter_func(filter_name))
|
||||||
return ml
|
return ml
|
||||||
|
|
||||||
def _todayFilters(self):
|
def _todayFilters(self):
|
||||||
|
@ -1024,9 +1001,19 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
subm.addChild(
|
subm.addChild(
|
||||||
self._simpleFilters(
|
self._simpleFilters(
|
||||||
(
|
(
|
||||||
(tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY),
|
(tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)),
|
||||||
(tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY),
|
(
|
||||||
(tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY),
|
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(
|
subm.addChild(
|
||||||
self._simpleFilters(
|
self._simpleFilters(
|
||||||
(
|
(
|
||||||
(tr(TR.ACTIONS_NEW), NamedFilter.NEW),
|
(
|
||||||
(tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN),
|
tr(TR.ACTIONS_NEW),
|
||||||
(tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW),
|
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
|
||||||
(tr(TR.FILTERING_IS_DUE), NamedFilter.DUE),
|
),
|
||||||
|
(
|
||||||
|
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,
|
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,
|
None,
|
||||||
(tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG),
|
(tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=SearchTerm.FLAG_RED)),
|
||||||
(tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG),
|
(
|
||||||
(tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG),
|
tr(TR.ACTIONS_ORANGE_FLAG),
|
||||||
(tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG),
|
SearchTerm(flag=SearchTerm.FLAG_ORANGE),
|
||||||
(tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG),
|
),
|
||||||
(tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG),
|
(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 = self.form.tableView
|
||||||
tv.selectionModel().clear()
|
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)
|
self.search_for(search)
|
||||||
|
|
||||||
tv.selectAll()
|
tv.selectAll()
|
||||||
|
@ -1593,17 +1603,6 @@ where id in %s"""
|
||||||
# Edit: finding dupes
|
# 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):
|
def onFindDupes(self):
|
||||||
self.editor.saveNow(self._onFindDupes)
|
self.editor.saveNow(self._onFindDupes)
|
||||||
|
|
||||||
|
@ -1648,7 +1647,12 @@ where id in %s"""
|
||||||
|
|
||||||
def duplicatesReport(self, web, fname, search, frm, web_context):
|
def duplicatesReport(self, web, fname, search, frm, web_context):
|
||||||
self.mw.progress.start()
|
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:
|
if not self._dupesButton:
|
||||||
self._dupesButton = b = frm.buttonBox.addButton(
|
self._dupesButton = b = frm.buttonBox.addButton(
|
||||||
tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole
|
tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole
|
||||||
|
@ -1665,7 +1669,11 @@ where id in %s"""
|
||||||
t += (
|
t += (
|
||||||
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
|
"""<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)),
|
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
|
||||||
html.escape(val),
|
html.escape(val),
|
||||||
)
|
)
|
||||||
|
@ -1761,9 +1769,10 @@ where id in %s"""
|
||||||
|
|
||||||
def focusCid(self, cid):
|
def focusCid(self, cid):
|
||||||
try:
|
try:
|
||||||
row = self.model.cards.index(cid)
|
row = list(self.model.cards).index(cid)
|
||||||
except:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
self.form.tableView.clearSelection()
|
||||||
self.form.tableView.selectRow(row)
|
self.form.tableView.selectRow(row)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki.collection import SearchTerm
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
|
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)
|
dyn = self.mw.col.decks.get(did)
|
||||||
# and then set various options
|
# and then set various options
|
||||||
if i == RADIO_FORGOT:
|
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
|
dyn["resched"] = False
|
||||||
elif i == RADIO_AHEAD:
|
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
|
dyn["resched"] = True
|
||||||
elif i == RADIO_PREVIEW:
|
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
|
dyn["resched"] = False
|
||||||
elif i == RADIO_CRAM:
|
elif i == RADIO_CRAM:
|
||||||
type = f.cardType.currentRow()
|
type = f.cardType.currentRow()
|
||||||
if type == TYPE_NEW:
|
if type == TYPE_NEW:
|
||||||
terms = "is:new "
|
terms = self.mw.col.build_search_string(
|
||||||
|
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
|
||||||
|
)
|
||||||
ord = DYN_ADDED
|
ord = DYN_ADDED
|
||||||
dyn["resched"] = True
|
dyn["resched"] = True
|
||||||
elif type == TYPE_DUE:
|
elif type == TYPE_DUE:
|
||||||
terms = "is:due "
|
terms = self.mw.col.build_search_string(
|
||||||
|
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
|
||||||
|
)
|
||||||
ord = DYN_DUE
|
ord = DYN_DUE
|
||||||
dyn["resched"] = True
|
dyn["resched"] = True
|
||||||
elif type == TYPE_REVIEW:
|
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
|
ord = DYN_RANDOM
|
||||||
dyn["resched"] = True
|
dyn["resched"] = True
|
||||||
else:
|
else:
|
||||||
|
@ -187,7 +204,9 @@ class CustomStudy(QDialog):
|
||||||
dyn["resched"] = False
|
dyn["resched"] = False
|
||||||
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
|
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
|
||||||
# add deck limit
|
# 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)
|
self.mw.col.decks.save(dyn)
|
||||||
# generate cards
|
# generate cards
|
||||||
self.created_custom_study = True
|
self.created_custom_study = True
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki.collection import InvalidInput, SearchTerm
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.rsbackend import InvalidInput
|
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
TR,
|
TR,
|
||||||
|
@ -47,8 +47,14 @@ class DeckConf(QDialog):
|
||||||
self.initialSetup()
|
self.initialSetup()
|
||||||
self.loadConf()
|
self.loadConf()
|
||||||
if search:
|
if search:
|
||||||
self.form.search.setText(search + " is:due")
|
search = self.mw.col.build_search_string(
|
||||||
self.form.search_2.setText(search + " is:new")
|
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()
|
self.form.search.selectAll()
|
||||||
|
|
||||||
if self.mw.col.schedVer() == 1:
|
if self.mw.col.schedVer() == 1:
|
||||||
|
@ -119,11 +125,11 @@ class DeckConf(QDialog):
|
||||||
else:
|
else:
|
||||||
d["delays"] = None
|
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()]]
|
terms = [[search, f.limit.value(), f.order.currentIndex()]]
|
||||||
|
|
||||||
if f.secondFilter.isChecked():
|
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()])
|
terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()])
|
||||||
|
|
||||||
d["terms"] = terms
|
d["terms"] = terms
|
||||||
|
|
|
@ -21,6 +21,7 @@ from bs4 import BeautifulSoup
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.sound
|
import aqt.sound
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
from anki.collection import SearchTerm
|
||||||
from anki.consts import MODEL_CLOZE
|
from anki.consts import MODEL_CLOZE
|
||||||
from anki.hooks import runFilter
|
from anki.hooks import runFilter
|
||||||
from anki.httpclient import HttpClient
|
from anki.httpclient import HttpClient
|
||||||
|
@ -537,8 +538,13 @@ class Editor:
|
||||||
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
||||||
|
|
||||||
def showDupes(self):
|
def showDupes(self):
|
||||||
browser = aqt.dialogs.open("Browser", self.mw)
|
self.mw.browser_search(
|
||||||
browser.search_dupe(self.note.model()["id"], self.note.fields[0])
|
SearchTerm(
|
||||||
|
dupe=SearchTerm.Dupe(
|
||||||
|
notetype_id=self.note.model()["id"], first_field=self.note.fields[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def fieldsAreBlank(self, previousNote=None):
|
def fieldsAreBlank(self, previousNote=None):
|
||||||
if not self.note:
|
if not self.note:
|
||||||
|
|
|
@ -66,9 +66,7 @@ class EmptyCardsDialog(QDialog):
|
||||||
self._delete_button.clicked.connect(self._on_delete)
|
self._delete_button.clicked.connect(self._on_delete)
|
||||||
|
|
||||||
def _on_note_link_clicked(self, link):
|
def _on_note_link_clicked(self, link):
|
||||||
browser = aqt.dialogs.open("Browser", self.mw)
|
self.mw.browser_search(link)
|
||||||
browser.form.searchEdit.lineEdit().setText(link)
|
|
||||||
browser.onSearchActivated()
|
|
||||||
|
|
||||||
def _on_delete(self):
|
def _on_delete(self):
|
||||||
self.mw.progress.start()
|
self.mw.progress.start()
|
||||||
|
|
|
@ -107,13 +107,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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">
|
<item row="0" column="0">
|
||||||
<widget class="QPushButton" name="filter">
|
<widget class="QPushButton" name="filter">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -158,12 +151,12 @@
|
||||||
<attribute name="horizontalHeaderCascadingSectionResizes">
|
<attribute name="horizontalHeaderCascadingSectionResizes">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
|
||||||
<number>20</number>
|
|
||||||
</attribute>
|
|
||||||
<attribute name="horizontalHeaderHighlightSections">
|
<attribute name="horizontalHeaderHighlightSections">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
|
<number>20</number>
|
||||||
|
</attribute>
|
||||||
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
@ -223,7 +216,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>750</width>
|
<width>750</width>
|
||||||
<height>22</height>
|
<height>21</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuEdit">
|
<widget class="QMenu" name="menuEdit">
|
||||||
|
|
|
@ -26,7 +26,7 @@ import aqt.stats
|
||||||
import aqt.toolbar
|
import aqt.toolbar
|
||||||
import aqt.webview
|
import aqt.webview
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection, SearchTerm
|
||||||
from anki.decks import Deck
|
from anki.decks import Deck
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
|
@ -1045,7 +1045,8 @@ title="%s" %s>%s</button>""" % (
|
||||||
aqt.dialogs.open("AddCards", self)
|
aqt.dialogs.open("AddCards", self)
|
||||||
|
|
||||||
def onBrowse(self) -> None:
|
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):
|
def onEditCurrent(self):
|
||||||
aqt.dialogs.open("EditCurrent", self)
|
aqt.dialogs.open("EditCurrent", self)
|
||||||
|
@ -1141,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
deck = self.col.decks.current()
|
deck = self.col.decks.current()
|
||||||
if not search:
|
if not search:
|
||||||
if not deck["dyn"]:
|
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(
|
while self.col.decks.id_for_name(
|
||||||
without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n))
|
without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n))
|
||||||
):
|
):
|
||||||
|
@ -1617,3 +1618,14 @@ title="%s" %s>%s</button>""" % (
|
||||||
|
|
||||||
def serverURL(self) -> str:
|
def serverURL(self) -> str:
|
||||||
return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
|
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
|
from typing import Iterable, List, Optional, Sequence, TypeVar
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki.collection import SearchTerm
|
||||||
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
|
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
|
@ -145,9 +146,7 @@ class MediaChecker:
|
||||||
|
|
||||||
if out is not None:
|
if out is not None:
|
||||||
nid, err = out
|
nid, err = out
|
||||||
browser = aqt.dialogs.open("Browser", self.mw)
|
self.mw.browser_search(SearchTerm(nid=nid))
|
||||||
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
|
|
||||||
browser.onSearchActivated()
|
|
||||||
showText(err, type="html")
|
showText(err, type="html")
|
||||||
else:
|
else:
|
||||||
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))
|
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))
|
||||||
|
|
|
@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki.collection import SearchTerm
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.sound import av_player
|
from aqt.sound import av_player
|
||||||
from aqt.toolbar import BottomBar
|
from aqt.toolbar import BottomBar
|
||||||
|
@ -71,8 +72,8 @@ class Overview:
|
||||||
elif url == "opts":
|
elif url == "opts":
|
||||||
self.mw.onDeckConf()
|
self.mw.onDeckConf()
|
||||||
elif url == "cram":
|
elif url == "cram":
|
||||||
deck = self.mw.col.decks.current()
|
deck = self.mw.col.decks.current()["name"]
|
||||||
self.mw.onCram("'deck:%s'" % deck["name"])
|
self.mw.onCram(self.mw.col.build_search_string(SearchTerm(deck=deck)))
|
||||||
elif url == "refresh":
|
elif url == "refresh":
|
||||||
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
|
|
@ -9,15 +9,9 @@ from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import ConfigBoolKey
|
from anki.collection import ConfigBoolKey, InvalidInput, SearchTerm
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import DeckTreeNode, TagTreeNode
|
||||||
DeckTreeNode,
|
|
||||||
FilterToSearchIn,
|
|
||||||
InvalidInput,
|
|
||||||
NamedFilter,
|
|
||||||
TagTreeNode,
|
|
||||||
)
|
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.main import ResetReason
|
from aqt.main import ResetReason
|
||||||
from aqt.models import Models
|
from aqt.models import Models
|
||||||
|
@ -42,6 +36,7 @@ class SidebarItemType(Enum):
|
||||||
COLLECTION = 1
|
COLLECTION = 1
|
||||||
CURRENT_DECK = 2
|
CURRENT_DECK = 2
|
||||||
SAVED_SEARCH = 3
|
SAVED_SEARCH = 3
|
||||||
|
FILTER = 3 # legacy alias for SAVED_SEARCH
|
||||||
DECK = 4
|
DECK = 4
|
||||||
NOTETYPE = 5
|
NOTETYPE = 5
|
||||||
TAG = 6
|
TAG = 6
|
||||||
|
@ -466,14 +461,12 @@ class SidebarTreeView(QTreeView):
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
tr(TR.BROWSING_WHOLE_COLLECTION),
|
tr(TR.BROWSING_WHOLE_COLLECTION),
|
||||||
":/icons/collection.svg",
|
":/icons/collection.svg",
|
||||||
self._named_filter(NamedFilter.WHOLE_COLLECTION),
|
|
||||||
item_type=SidebarItemType.COLLECTION,
|
item_type=SidebarItemType.COLLECTION,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
tr(TR.BROWSING_CURRENT_DECK),
|
tr(TR.BROWSING_CURRENT_DECK),
|
||||||
":/icons/deck.svg",
|
":/icons/deck.svg",
|
||||||
self._named_filter(NamedFilter.CURRENT_DECK),
|
|
||||||
item_type=SidebarItemType.CURRENT_DECK,
|
item_type=SidebarItemType.CURRENT_DECK,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
@ -499,8 +492,8 @@ class SidebarTreeView(QTreeView):
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
self._saved_filter(filt),
|
self._filter_func(filt),
|
||||||
item_type=SidebarItemType.SAVED_SEARCH,
|
item_type=SidebarItemType.FILTER,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
|
||||||
|
@ -519,7 +512,7 @@ class SidebarTreeView(QTreeView):
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
node.name,
|
node.name,
|
||||||
icon,
|
icon,
|
||||||
self._tag_filter(head + node.name),
|
self._filter_func(SearchTerm(tag=head + node.name)),
|
||||||
toggle_expand(),
|
toggle_expand(),
|
||||||
not node.collapsed,
|
not node.collapsed,
|
||||||
item_type=SidebarItemType.TAG,
|
item_type=SidebarItemType.TAG,
|
||||||
|
@ -551,7 +544,7 @@ class SidebarTreeView(QTreeView):
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
node.name,
|
node.name,
|
||||||
icon,
|
icon,
|
||||||
self._deck_filter(head + node.name),
|
self._filter_func(SearchTerm(deck=head + node.name)),
|
||||||
toggle_expand(),
|
toggle_expand(),
|
||||||
not node.collapsed,
|
not node.collapsed,
|
||||||
item_type=SidebarItemType.DECK,
|
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()):
|
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
nt["name"],
|
nt["name"],
|
||||||
icon=icon,
|
icon,
|
||||||
onClick=self._note_filter(nt["name"]),
|
self._filter_func(SearchTerm(note=nt["name"])),
|
||||||
item_type=SidebarItemType.NOTETYPE,
|
item_type=SidebarItemType.NOTETYPE,
|
||||||
id=nt["id"],
|
id=nt["id"],
|
||||||
)
|
)
|
||||||
|
@ -594,7 +587,9 @@ class SidebarTreeView(QTreeView):
|
||||||
child = SidebarItem(
|
child = SidebarItem(
|
||||||
tmpl["name"],
|
tmpl["name"],
|
||||||
icon,
|
icon,
|
||||||
onClick=self._template_filter(nt["name"], c),
|
self._filter_func(
|
||||||
|
SearchTerm(note=nt["name"]), SearchTerm(template=c)
|
||||||
|
),
|
||||||
item_type=SidebarItemType.TEMPLATE,
|
item_type=SidebarItemType.TEMPLATE,
|
||||||
full_name=nt["name"] + "::" + tmpl["name"],
|
full_name=nt["name"] + "::" + tmpl["name"],
|
||||||
)
|
)
|
||||||
|
@ -602,34 +597,8 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
|
||||||
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
|
def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
|
||||||
return lambda: self.browser.update_search(
|
return lambda: self.browser.update_search(self.col.build_search_string(*terms))
|
||||||
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)
|
|
||||||
|
|
||||||
# Context menu actions
|
# Context menu actions
|
||||||
###########################
|
###########################
|
||||||
|
@ -807,7 +776,7 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
def save_current_search(self, _item=None) -> None:
|
def save_current_search(self, _item=None) -> None:
|
||||||
try:
|
try:
|
||||||
filt = self.col.backend.normalize_search(
|
filt = self.col.build_search_string(
|
||||||
self.browser.form.searchEdit.lineEdit().text()
|
self.browser.form.searchEdit.lineEdit().text()
|
||||||
)
|
)
|
||||||
except InvalidInput as e:
|
except InvalidInput as e:
|
||||||
|
|
|
@ -84,7 +84,7 @@ service BackendService {
|
||||||
|
|
||||||
// searching
|
// searching
|
||||||
|
|
||||||
rpc FilterToSearch(FilterToSearchIn) returns (String);
|
rpc FilterToSearch(SearchTerm) returns (String);
|
||||||
rpc NormalizeSearch(String) returns (String);
|
rpc NormalizeSearch(String) returns (String);
|
||||||
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
||||||
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
||||||
|
@ -765,37 +765,58 @@ message BuiltinSearchOrder {
|
||||||
bool reverse = 2;
|
bool reverse = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FilterToSearchIn {
|
message SearchTerm {
|
||||||
enum NamedFilter {
|
message Dupe {
|
||||||
WHOLE_COLLECTION = 0;
|
int64 notetype_id = 1;
|
||||||
CURRENT_DECK = 1;
|
string first_field = 2;
|
||||||
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 {
|
enum Flag {
|
||||||
NoteTypeID mid = 1;
|
FLAG_NONE = 0;
|
||||||
string text = 2;
|
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 {
|
oneof filter {
|
||||||
NamedFilter name = 1;
|
string tag = 1;
|
||||||
string tag = 2;
|
string deck = 2;
|
||||||
string deck = 3;
|
string note = 3;
|
||||||
string note = 4;
|
uint32 template = 4;
|
||||||
uint32 template = 5;
|
int64 nid = 5;
|
||||||
DupeIn dupe = 6;
|
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},
|
sched::timespan::{answer_button_time, time_span},
|
||||||
search::{
|
search::{
|
||||||
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
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,
|
stats::studied_today,
|
||||||
sync::{
|
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 {
|
impl From<pb::NoteTypeId> for NoteTypeID {
|
||||||
fn from(ntid: pb::NoteTypeId) -> Self {
|
fn from(ntid: pb::NoteTypeId) -> Self {
|
||||||
NoteTypeID(ntid.ntid)
|
NoteTypeID(ntid.ntid)
|
||||||
|
@ -280,41 +291,11 @@ impl From<pb::DeckConfigId> for DeckConfID {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<pb::FilterToSearchIn> for Node<'_> {
|
impl From<pb::SearchTerm> for Node<'_> {
|
||||||
fn from(msg: pb::FilterToSearchIn) -> Self {
|
fn from(msg: pb::SearchTerm) -> Self {
|
||||||
use pb::filter_to_search_in::Filter;
|
use pb::search_term::Filter;
|
||||||
use pb::filter_to_search_in::NamedFilter;
|
use pb::search_term::Flag;
|
||||||
match msg
|
match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
|
||||||
.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)))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Filter::Tag(s) => Node::Search(SearchNode::Tag(
|
Filter::Tag(s) => Node::Search(SearchNode::Tag(
|
||||||
escape_anki_wildcards(&s).into_owned().into(),
|
escape_anki_wildcards(&s).into_owned().into(),
|
||||||
)),
|
)),
|
||||||
|
@ -327,10 +308,41 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
|
||||||
Filter::Template(u) => {
|
Filter::Template(u) => {
|
||||||
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
|
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 {
|
Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates {
|
||||||
note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(),
|
note_type_id: dupe.notetype_id.into(),
|
||||||
text: dupe.text.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 {
|
impl BackendService for Backend {
|
||||||
fn latest_progress(&self, _input: Empty) -> BackendResult<pb::Progress> {
|
fn latest_progress(&self, _input: Empty) -> BackendResult<pb::Progress> {
|
||||||
let progress = self.progress_state.lock().unwrap().last_progress;
|
let progress = self.progress_state.lock().unwrap().last_progress;
|
||||||
|
@ -466,7 +504,7 @@ impl BackendService for Backend {
|
||||||
// searching
|
// 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())
|
Ok(write_nodes(&[input.into()]).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod sqlwriter;
|
||||||
mod writer;
|
mod writer;
|
||||||
|
|
||||||
pub use cards::SortMode;
|
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::{
|
pub use writer::{
|
||||||
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||||
BoolSeparator,
|
BoolSeparator,
|
||||||
|
|
|
@ -58,7 +58,7 @@ pub enum SearchNode<'a> {
|
||||||
NoteType(Cow<'a, str>),
|
NoteType(Cow<'a, str>),
|
||||||
Rated {
|
Rated {
|
||||||
days: u32,
|
days: u32,
|
||||||
ease: EaseKind,
|
ease: RatingKind,
|
||||||
},
|
},
|
||||||
Tag(Cow<'a, str>),
|
Tag(Cow<'a, str>),
|
||||||
Duplicates {
|
Duplicates {
|
||||||
|
@ -67,7 +67,7 @@ pub enum SearchNode<'a> {
|
||||||
},
|
},
|
||||||
State(StateKind),
|
State(StateKind),
|
||||||
Flag(u8),
|
Flag(u8),
|
||||||
NoteIDs(&'a str),
|
NoteIDs(Cow<'a, str>),
|
||||||
CardIDs(&'a str),
|
CardIDs(&'a str),
|
||||||
Property {
|
Property {
|
||||||
operator: String,
|
operator: String,
|
||||||
|
@ -87,7 +87,7 @@ pub enum PropertyKind {
|
||||||
Lapses(u32),
|
Lapses(u32),
|
||||||
Ease(f32),
|
Ease(f32),
|
||||||
Position(u32),
|
Position(u32),
|
||||||
Rated(i32, EaseKind),
|
Rated(i32, RatingKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
@ -109,7 +109,7 @@ pub enum TemplateKind<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum EaseKind {
|
pub enum RatingKind {
|
||||||
AnswerButton(u8),
|
AnswerButton(u8),
|
||||||
AnyAnswerButton,
|
AnyAnswerButton,
|
||||||
ManualReschedule,
|
ManualReschedule,
|
||||||
|
@ -318,7 +318,7 @@ fn search_node_for_text_with_argument<'a>(
|
||||||
"is" => parse_state(val)?,
|
"is" => parse_state(val)?,
|
||||||
"did" => parse_did(val)?,
|
"did" => parse_did(val)?,
|
||||||
"mid" => parse_mid(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)?),
|
"cid" => SearchNode::CardIDs(check_id_list(val, key)?),
|
||||||
"re" => SearchNode::Regex(unescape_quotes(val)),
|
"re" => SearchNode::Regex(unescape_quotes(val)),
|
||||||
"nc" => SearchNode::NoCombining(unescape(val)?),
|
"nc" => SearchNode::NoCombining(unescape(val)?),
|
||||||
|
@ -353,7 +353,7 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
||||||
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
||||||
parse_u32(s, "resched:").map(|days| SearchNode::Rated {
|
parse_u32(s, "resched:").map(|days| SearchNode::Rated {
|
||||||
days,
|
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)?,
|
"rated" => parse_prop_rated(num, prop_clause)?,
|
||||||
"resched" => PropertyKind::Rated(
|
"resched" => PropertyKind::Rated(
|
||||||
parse_negative_i32(num, prop_clause)?,
|
parse_negative_i32(num, prop_clause)?,
|
||||||
EaseKind::ManualReschedule,
|
RatingKind::ManualReschedule,
|
||||||
),
|
),
|
||||||
"ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
|
"ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
|
||||||
"reps" => PropertyKind::Reps(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 {
|
Ok(if let Some(num) = num {
|
||||||
EaseKind::AnswerButton(
|
RatingKind::AnswerButton(
|
||||||
num.parse()
|
num.parse()
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
.and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { 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 {
|
} else {
|
||||||
EaseKind::AnyAnswerButton
|
RatingKind::AnyAnswerButton
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -813,7 +813,7 @@ mod test {
|
||||||
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
|
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse("nid:1237123712,2,3")?,
|
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("is:due")?, vec![Search(State(StateKind::Due))]);
|
||||||
assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]);
|
assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// 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::{
|
use crate::{
|
||||||
card::{CardQueue, CardType},
|
card::{CardQueue, CardType},
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
|
@ -211,7 +211,7 @@ impl SqlWriter<'_> {
|
||||||
Ok(())
|
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 today_cutoff = self.col.timing_today()?.next_day_at;
|
||||||
let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000;
|
let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000;
|
||||||
let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000;
|
let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000;
|
||||||
|
@ -240,9 +240,9 @@ impl SqlWriter<'_> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match ease {
|
match ease {
|
||||||
EaseKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
|
RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
|
||||||
EaseKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
|
RatingKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
|
||||||
EaseKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
|
RatingKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
|
||||||
}
|
}
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
decks::DeckID as DeckIDType,
|
decks::DeckID as DeckIDType,
|
||||||
err::Result,
|
err::Result,
|
||||||
notetype::NoteTypeID as NoteTypeIDType,
|
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 itertools::Itertools;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
@ -154,8 +154,8 @@ fn write_template(template: &TemplateKind) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_rated(days: &u32, ease: &EaseKind) -> String {
|
fn write_rated(days: &u32, ease: &RatingKind) -> String {
|
||||||
use EaseKind::*;
|
use RatingKind::*;
|
||||||
match ease {
|
match ease {
|
||||||
AnswerButton(n) => format!("\"rated:{}:{}\"", days, n),
|
AnswerButton(n) => format!("\"rated:{}:{}\"", days, n),
|
||||||
AnyAnswerButton => format!("\"rated:{}\"", days),
|
AnyAnswerButton => format!("\"rated:{}\"", days),
|
||||||
|
@ -196,9 +196,9 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String {
|
||||||
Ease(f) => format!("\"prop:ease{}{}\"", operator, f),
|
Ease(f) => format!("\"prop:ease{}{}\"", operator, f),
|
||||||
Position(u) => format!("\"prop:pos{}{}\"", operator, u),
|
Position(u) => format!("\"prop:pos{}{}\"", operator, u),
|
||||||
Rated(u, ease) => match ease {
|
Rated(u, ease) => match ease {
|
||||||
EaseKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
|
RatingKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
|
||||||
EaseKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
|
RatingKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
|
||||||
EaseKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
|
RatingKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue