Merge branch 'master' of https://github.com/ankitects/anki into tagtree
This commit is contained in:
commit
72e8f9d640
@ -47,6 +47,8 @@ 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
|
||||||
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
|
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
|
||||||
SyncAuth = pb.SyncAuth
|
SyncAuth = pb.SyncAuth
|
||||||
SyncOutput = pb.SyncCollectionOut
|
SyncOutput = pb.SyncCollectionOut
|
||||||
|
@ -57,6 +57,7 @@ fn want_release_gil(method: u32) -> bool {
|
|||||||
| BackendMethod::NegateSearch
|
| BackendMethod::NegateSearch
|
||||||
| BackendMethod::ConcatenateSearches
|
| BackendMethod::ConcatenateSearches
|
||||||
| BackendMethod::ReplaceSearchTerm
|
| BackendMethod::ReplaceSearchTerm
|
||||||
|
| BackendMethod::FilterToSearch
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -193,6 +193,7 @@ def test_findCards():
|
|||||||
assert len(col.findCards("-prop:ease>2")) > 1
|
assert len(col.findCards("-prop:ease>2")) > 1
|
||||||
# recently failed
|
# recently failed
|
||||||
if not isNearCutoff():
|
if not isNearCutoff():
|
||||||
|
# rated
|
||||||
assert len(col.findCards("rated:1:1")) == 0
|
assert len(col.findCards("rated:1:1")) == 0
|
||||||
assert len(col.findCards("rated:1:2")) == 0
|
assert len(col.findCards("rated:1:2")) == 0
|
||||||
c = col.sched.getCard()
|
c = col.sched.getCard()
|
||||||
@ -204,13 +205,14 @@ def test_findCards():
|
|||||||
assert len(col.findCards("rated:1:1")) == 1
|
assert len(col.findCards("rated:1:1")) == 1
|
||||||
assert len(col.findCards("rated:1:2")) == 1
|
assert len(col.findCards("rated:1:2")) == 1
|
||||||
assert len(col.findCards("rated:1")) == 2
|
assert len(col.findCards("rated:1")) == 2
|
||||||
assert len(col.findCards("rated:0:2")) == 0
|
|
||||||
assert len(col.findCards("rated:2:2")) == 1
|
assert len(col.findCards("rated:2:2")) == 1
|
||||||
|
assert len(col.findCards("rated:0")) == len(col.findCards("rated:1"))
|
||||||
|
|
||||||
# added
|
# added
|
||||||
assert len(col.findCards("added:0")) == 0
|
|
||||||
col.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
|
col.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
|
||||||
assert len(col.findCards("added:1")) == col.cardCount() - 1
|
assert len(col.findCards("added:1")) == col.cardCount() - 1
|
||||||
assert len(col.findCards("added:2")) == col.cardCount()
|
assert len(col.findCards("added:2")) == col.cardCount()
|
||||||
|
assert len(col.findCards("added:0")) == len(col.findCards("added:1"))
|
||||||
else:
|
else:
|
||||||
print("some find tests disabled near cutoff")
|
print("some find tests disabled near cutoff")
|
||||||
# empty field
|
# empty field
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -21,7 +20,14 @@ 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 ConcatSeparator, DeckTreeNode, InvalidInput, TagTreeNode
|
from anki.rsbackend import (
|
||||||
|
ConcatSeparator,
|
||||||
|
DeckTreeNode,
|
||||||
|
FilterToSearchIn,
|
||||||
|
InvalidInput,
|
||||||
|
NamedFilter,
|
||||||
|
TagTreeNode,
|
||||||
|
)
|
||||||
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
|
||||||
@ -623,12 +629,6 @@ class Browser(QMainWindow):
|
|||||||
# pylint: disable=unnecessary-lambda
|
# pylint: disable=unnecessary-lambda
|
||||||
# actions
|
# actions
|
||||||
f = self.form
|
f = self.form
|
||||||
qconnect(f.previewButton.clicked, self.onTogglePreview)
|
|
||||||
f.previewButton.setToolTip(
|
|
||||||
tr(TR.BROWSING_PREVIEW_SELECTED_CARD, val=shortcut("Ctrl+Shift+P"))
|
|
||||||
)
|
|
||||||
f.previewButton.setShortcut("Ctrl+Shift+P")
|
|
||||||
|
|
||||||
qconnect(f.filter.clicked, self.onFilterButton)
|
qconnect(f.filter.clicked, self.onFilterButton)
|
||||||
# edit
|
# edit
|
||||||
qconnect(f.actionUndo.triggered, self.mw.onUndo)
|
qconnect(f.actionUndo.triggered, self.mw.onUndo)
|
||||||
@ -873,7 +873,30 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
self.singleCard = False
|
self.singleCard = False
|
||||||
|
|
||||||
def setupEditor(self):
|
def setupEditor(self):
|
||||||
|
def add_preview_button(leftbuttons, editor):
|
||||||
|
preview_shortcut = "Ctrl+Shift+P"
|
||||||
|
leftbuttons.insert(
|
||||||
|
0,
|
||||||
|
editor.addButton(
|
||||||
|
None,
|
||||||
|
"preview",
|
||||||
|
lambda _editor: self.onTogglePreview(),
|
||||||
|
tr(
|
||||||
|
TR.BROWSING_PREVIEW_SELECTED_CARD,
|
||||||
|
val=shortcut(preview_shortcut),
|
||||||
|
),
|
||||||
|
tr(TR.ACTIONS_PREVIEW),
|
||||||
|
id="previewButton",
|
||||||
|
keys=preview_shortcut,
|
||||||
|
disables=False,
|
||||||
|
rightside=False,
|
||||||
|
toggleable=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
gui_hooks.editor_did_init_left_buttons.append(add_preview_button)
|
||||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
||||||
|
gui_hooks.editor_did_init_left_buttons.remove(add_preview_button)
|
||||||
|
|
||||||
def onRowChanged(self, current, previous):
|
def onRowChanged(self, current, previous):
|
||||||
"Update current note and hide/show editor."
|
"Update current note and hide/show editor."
|
||||||
@ -1111,14 +1134,14 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
tr(TR.BROWSING_WHOLE_COLLECTION),
|
tr(TR.BROWSING_WHOLE_COLLECTION),
|
||||||
":/icons/collection.svg",
|
":/icons/collection.svg",
|
||||||
self._filterFunc(""),
|
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._filterFunc("deck:current"),
|
self._named_filter(NamedFilter.CURRENT_DECK),
|
||||||
item_type=SidebarItemType.CURRENT_DECK,
|
item_type=SidebarItemType.CURRENT_DECK,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
@ -1130,7 +1153,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
name,
|
name,
|
||||||
":/icons/heart.svg",
|
":/icons/heart.svg",
|
||||||
lambda s=filt: self.setFilter(s), # type: ignore
|
self._saved_filter(filt),
|
||||||
item_type=SidebarItemType.FILTER,
|
item_type=SidebarItemType.FILTER,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
@ -1154,7 +1177,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
node.name,
|
node.name,
|
||||||
":/icons/tag.svg",
|
":/icons/tag.svg",
|
||||||
set_filter(),
|
self._tag_filter(head + node.name),
|
||||||
toggle_expand(),
|
toggle_expand(),
|
||||||
not node.collapsed,
|
not node.collapsed,
|
||||||
item_type=SidebarItemType.TAG,
|
item_type=SidebarItemType.TAG,
|
||||||
@ -1172,10 +1195,6 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
def fillGroups(root, nodes: Sequence[DeckTreeNode], head=""):
|
def fillGroups(root, nodes: Sequence[DeckTreeNode], head=""):
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
||||||
def set_filter():
|
|
||||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
|
||||||
return lambda: self.setFilter("deck", full_name)
|
|
||||||
|
|
||||||
def toggle_expand():
|
def toggle_expand():
|
||||||
did = node.deck_id # pylint: disable=cell-var-from-loop
|
did = node.deck_id # pylint: disable=cell-var-from-loop
|
||||||
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
||||||
@ -1183,7 +1202,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
node.name,
|
node.name,
|
||||||
":/icons/deck.svg",
|
":/icons/deck.svg",
|
||||||
set_filter(),
|
self._deck_filter(head + node.name),
|
||||||
toggle_expand(),
|
toggle_expand(),
|
||||||
not node.collapsed,
|
not node.collapsed,
|
||||||
item_type=SidebarItemType.DECK,
|
item_type=SidebarItemType.DECK,
|
||||||
@ -1201,7 +1220,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
m.name,
|
m.name,
|
||||||
":/icons/notetype.svg",
|
":/icons/notetype.svg",
|
||||||
lambda m=m: self.setFilter("note", m.name), # type: ignore
|
self._note_filter(m.name),
|
||||||
item_type=SidebarItemType.NOTETYPE,
|
item_type=SidebarItemType.NOTETYPE,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
@ -1229,47 +1248,36 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
|
|
||||||
ml.popupOver(self.form.filter)
|
ml.popupOver(self.form.filter)
|
||||||
|
|
||||||
def setFilter(self, *args):
|
def setFilter(self, *searches):
|
||||||
if len(args) == 1:
|
|
||||||
txt = args[0]
|
|
||||||
else:
|
|
||||||
txt = ""
|
|
||||||
items = []
|
|
||||||
for i, a in enumerate(args):
|
|
||||||
if i % 2 == 0:
|
|
||||||
txt += a + ":"
|
|
||||||
else:
|
|
||||||
txt += re.sub(r'["*_\\]', r"\\\g<0>", a)
|
|
||||||
txt = '"{}"'.format(txt.replace('"', '\\"'))
|
|
||||||
items.append(txt)
|
|
||||||
txt = ""
|
|
||||||
txt = " AND ".join(items)
|
|
||||||
try:
|
try:
|
||||||
if self.mw.app.keyboardModifiers() & Qt.AltModifier:
|
search = self.col.backend.concatenate_searches(
|
||||||
txt = self.col.backend.negate_search(txt)
|
sep=ConcatSeparator.AND, searches=searches
|
||||||
|
)
|
||||||
|
mods = self.mw.app.keyboardModifiers()
|
||||||
|
if mods & Qt.AltModifier:
|
||||||
|
search = self.col.backend.negate_search(search)
|
||||||
cur = str(self.form.searchEdit.lineEdit().text())
|
cur = str(self.form.searchEdit.lineEdit().text())
|
||||||
if cur != self._searchPrompt:
|
if cur != self._searchPrompt:
|
||||||
mods = self.mw.app.keyboardModifiers()
|
|
||||||
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
|
||||||
txt = self.col.backend.replace_search_term(
|
search = self.col.backend.replace_search_term(
|
||||||
search=cur, replacement=txt
|
search=cur, replacement=search
|
||||||
)
|
)
|
||||||
elif mods & Qt.ControlModifier:
|
elif mods & Qt.ControlModifier:
|
||||||
txt = self.col.backend.concatenate_searches(
|
search = self.col.backend.concatenate_searches(
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
sep=ConcatSeparator.AND,
|
sep=ConcatSeparator.AND,
|
||||||
searches=[cur, txt],
|
searches=[cur, search],
|
||||||
)
|
)
|
||||||
elif mods & Qt.ShiftModifier:
|
elif mods & Qt.ShiftModifier:
|
||||||
txt = self.col.backend.concatenate_searches(
|
search = self.col.backend.concatenate_searches(
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
sep=ConcatSeparator.OR,
|
sep=ConcatSeparator.OR,
|
||||||
searches=[cur, txt],
|
searches=[cur, search],
|
||||||
)
|
)
|
||||||
except InvalidInput as e:
|
except InvalidInput as e:
|
||||||
showWarning(str(e))
|
showWarning(str(e))
|
||||||
else:
|
else:
|
||||||
self.form.searchEdit.lineEdit().setText(txt)
|
self.form.searchEdit.lineEdit().setText(search)
|
||||||
self.onSearchActivated()
|
self.onSearchActivated()
|
||||||
|
|
||||||
def _simpleFilters(self, items):
|
def _simpleFilters(self, items):
|
||||||
@ -1278,18 +1286,44 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
if row is None:
|
if row is None:
|
||||||
ml.addSeparator()
|
ml.addSeparator()
|
||||||
else:
|
else:
|
||||||
label, filter = row
|
label, filter_name = row
|
||||||
ml.addItem(label, self._filterFunc(filter))
|
ml.addItem(label, self._named_filter(filter_name))
|
||||||
return ml
|
return ml
|
||||||
|
|
||||||
def _filterFunc(self, *args):
|
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
|
||||||
return lambda *, f=args: self.setFilter(*f)
|
return lambda: self.setFilter(
|
||||||
|
self.col.backend.filter_to_search(FilterToSearchIn(name=name))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _tag_filter(self, tag: str) -> Callable:
|
||||||
|
return lambda: self.setFilter(
|
||||||
|
self.col.backend.filter_to_search(FilterToSearchIn(tag=tag))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _deck_filter(self, deck: str) -> Callable:
|
||||||
|
return lambda: self.setFilter(
|
||||||
|
self.col.backend.filter_to_search(FilterToSearchIn(deck=deck))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _note_filter(self, note: str) -> Callable:
|
||||||
|
return lambda: self.setFilter(
|
||||||
|
self.col.backend.filter_to_search(FilterToSearchIn(note=note))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _template_filter(self, note: str, template: int) -> Callable:
|
||||||
|
return lambda: self.setFilter(
|
||||||
|
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.setFilter(saved)
|
||||||
|
|
||||||
def _commonFilters(self):
|
def _commonFilters(self):
|
||||||
return self._simpleFilters(
|
return self._simpleFilters(
|
||||||
(
|
(
|
||||||
(tr(TR.BROWSING_WHOLE_COLLECTION), ""),
|
(tr(TR.BROWSING_WHOLE_COLLECTION), NamedFilter.WHOLE_COLLECTION),
|
||||||
(tr(TR.BROWSING_CURRENT_DECK), '"deck:current"'),
|
(tr(TR.BROWSING_CURRENT_DECK), NamedFilter.CURRENT_DECK),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1298,9 +1332,9 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
subm.addChild(
|
subm.addChild(
|
||||||
self._simpleFilters(
|
self._simpleFilters(
|
||||||
(
|
(
|
||||||
(tr(TR.BROWSING_ADDED_TODAY), '"added:1"'),
|
(tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY),
|
||||||
(tr(TR.BROWSING_STUDIED_TODAY), '"rated:1"'),
|
(tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY),
|
||||||
(tr(TR.BROWSING_AGAIN_TODAY), '"rated:1:1"'),
|
(tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1311,20 +1345,20 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
subm.addChild(
|
subm.addChild(
|
||||||
self._simpleFilters(
|
self._simpleFilters(
|
||||||
(
|
(
|
||||||
(tr(TR.ACTIONS_NEW), '"is:new"'),
|
(tr(TR.ACTIONS_NEW), NamedFilter.NEW),
|
||||||
(tr(TR.SCHEDULING_LEARNING), '"is:learn"'),
|
(tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN),
|
||||||
(tr(TR.SCHEDULING_REVIEW), '"is:review"'),
|
(tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW),
|
||||||
(tr(TR.FILTERING_IS_DUE), '"is:due"'),
|
(tr(TR.FILTERING_IS_DUE), NamedFilter.DUE),
|
||||||
None,
|
None,
|
||||||
(tr(TR.BROWSING_SUSPENDED), '"is:suspended"'),
|
(tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED),
|
||||||
(tr(TR.BROWSING_BURIED), '"is:buried"'),
|
(tr(TR.BROWSING_BURIED), NamedFilter.BURIED),
|
||||||
None,
|
None,
|
||||||
(tr(TR.ACTIONS_RED_FLAG), '"flag:1"'),
|
(tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG),
|
||||||
(tr(TR.ACTIONS_ORANGE_FLAG), '"flag:2"'),
|
(tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG),
|
||||||
(tr(TR.ACTIONS_GREEN_FLAG), '"flag:3"'),
|
(tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG),
|
||||||
(tr(TR.ACTIONS_BLUE_FLAG), '"flag:4"'),
|
(tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG),
|
||||||
(tr(TR.BROWSING_NO_FLAG), '"flag:0"'),
|
(tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG),
|
||||||
(tr(TR.BROWSING_ANY_FLAG), '"-flag:0"'),
|
(tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1341,7 +1375,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
|
|
||||||
tagList = MenuList()
|
tagList = MenuList()
|
||||||
for t in sorted(self.col.tags.all(), key=lambda s: s.lower()):
|
for t in sorted(self.col.tags.all(), key=lambda s: s.lower()):
|
||||||
tagList.addItem(self._escapeMenuItem(t), self._filterFunc("tag", t))
|
tagList.addItem(self._escapeMenuItem(t), self._tag_filter(t))
|
||||||
|
|
||||||
m.addChild(tagList.chunked())
|
m.addChild(tagList.chunked())
|
||||||
return m
|
return m
|
||||||
@ -1354,13 +1388,11 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
fullname = parent_prefix + node.name
|
fullname = parent_prefix + node.name
|
||||||
if node.children:
|
if node.children:
|
||||||
subm = parent.addMenu(escaped_name)
|
subm = parent.addMenu(escaped_name)
|
||||||
subm.addItem(
|
subm.addItem(tr(TR.ACTIONS_FILTER), self._deck_filter(fullname))
|
||||||
tr(TR.ACTIONS_FILTER), self._filterFunc("deck", fullname)
|
|
||||||
)
|
|
||||||
subm.addSeparator()
|
subm.addSeparator()
|
||||||
addDecks(subm, node.children, fullname + "::")
|
addDecks(subm, node.children, fullname + "::")
|
||||||
else:
|
else:
|
||||||
parent.addItem(escaped_name, self._filterFunc("deck", fullname))
|
parent.addItem(escaped_name, self._deck_filter(fullname))
|
||||||
|
|
||||||
alldecks = self.col.decks.deck_tree()
|
alldecks = self.col.decks.deck_tree()
|
||||||
ml = MenuList()
|
ml = MenuList()
|
||||||
@ -1382,12 +1414,12 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
escaped_nt_name = self._escapeMenuItem(nt["name"])
|
escaped_nt_name = self._escapeMenuItem(nt["name"])
|
||||||
# no sub menu if it's a single template
|
# no sub menu if it's a single template
|
||||||
if len(nt["tmpls"]) == 1:
|
if len(nt["tmpls"]) == 1:
|
||||||
noteTypes.addItem(escaped_nt_name, self._filterFunc("note", nt["name"]))
|
noteTypes.addItem(escaped_nt_name, self._note_filter(nt["name"]))
|
||||||
else:
|
else:
|
||||||
subm = noteTypes.addMenu(escaped_nt_name)
|
subm = noteTypes.addMenu(escaped_nt_name)
|
||||||
|
|
||||||
subm.addItem(
|
subm.addItem(
|
||||||
tr(TR.BROWSING_ALL_CARD_TYPES), self._filterFunc("note", nt["name"])
|
tr(TR.BROWSING_ALL_CARD_TYPES), self._note_filter(nt["name"])
|
||||||
)
|
)
|
||||||
subm.addSeparator()
|
subm.addSeparator()
|
||||||
|
|
||||||
@ -1400,9 +1432,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
num=c + 1,
|
num=c + 1,
|
||||||
name=self._escapeMenuItem(tmpl["name"]),
|
name=self._escapeMenuItem(tmpl["name"]),
|
||||||
)
|
)
|
||||||
subm.addItem(
|
subm.addItem(name, self._template_filter(nt["name"], c))
|
||||||
name, self._filterFunc("note", nt["name"], "card", str(c + 1))
|
|
||||||
)
|
|
||||||
|
|
||||||
m.addChild(noteTypes.chunked())
|
m.addChild(noteTypes.chunked())
|
||||||
return m
|
return m
|
||||||
@ -1429,7 +1459,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
|
|
||||||
ml.addSeparator()
|
ml.addSeparator()
|
||||||
for name, filt in sorted(saved.items()):
|
for name, filt in sorted(saved.items()):
|
||||||
ml.addItem(self._escapeMenuItem(name), self._filterFunc(filt))
|
ml.addItem(self._escapeMenuItem(name), self._saved_filter(filt))
|
||||||
|
|
||||||
return ml
|
return ml
|
||||||
|
|
||||||
@ -1579,7 +1609,10 @@ where id in %s"""
|
|||||||
|
|
||||||
def _renderPreview(self):
|
def _renderPreview(self):
|
||||||
if self._previewer:
|
if self._previewer:
|
||||||
self._previewer.render_card()
|
if self.singleCard:
|
||||||
|
self._previewer.render_card()
|
||||||
|
else:
|
||||||
|
self.onTogglePreview()
|
||||||
|
|
||||||
def _cleanup_preview(self):
|
def _cleanup_preview(self):
|
||||||
if self._previewer:
|
if self._previewer:
|
||||||
@ -1587,6 +1620,8 @@ where id in %s"""
|
|||||||
self._previewer.close()
|
self._previewer.close()
|
||||||
|
|
||||||
def _on_preview_closed(self):
|
def _on_preview_closed(self):
|
||||||
|
if self.editor.web:
|
||||||
|
self.editor.web.eval("$('#previewButton').removeClass('highlighted')")
|
||||||
self._previewer = None
|
self._previewer = None
|
||||||
|
|
||||||
# Card deletion
|
# Card deletion
|
||||||
|
@ -65,15 +65,33 @@ button.linkb {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nightMode & > img {
|
||||||
|
filter: invert(180);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.linkb:disabled {
|
button:focus {
|
||||||
opacity: 0.3;
|
outline: none;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.highlighted {
|
button.highlighted {
|
||||||
border-bottom: 3px solid #000;
|
.nightMode #topbutsleft & {
|
||||||
|
background: linear-gradient(0deg, #333333 0%, #434343 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbutsleft & {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbutsright & {
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#fields {
|
#fields {
|
||||||
@ -88,12 +106,6 @@ button.highlighted {
|
|||||||
color: var(--link);
|
color: var(--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nightMode {
|
|
||||||
button.linkb > img {
|
|
||||||
filter: invert(180);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawing {
|
.drawing {
|
||||||
zoom: 50%;
|
zoom: 50%;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
|
from random import randrange
|
||||||
from typing import Callable, List, Optional, Tuple
|
from typing import Callable, List, Optional, Tuple
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
@ -251,12 +252,29 @@ class Editor:
|
|||||||
"""Assign func to bridge cmd, register shortcut, return button"""
|
"""Assign func to bridge cmd, register shortcut, return button"""
|
||||||
if func:
|
if func:
|
||||||
self._links[cmd] = func
|
self._links[cmd] = func
|
||||||
if keys:
|
|
||||||
QShortcut( # type: ignore
|
if keys:
|
||||||
QKeySequence(keys),
|
|
||||||
self.widget,
|
def on_activated():
|
||||||
activated=lambda s=self: func(s),
|
func(self)
|
||||||
)
|
|
||||||
|
if toggleable:
|
||||||
|
# generate a random id for triggering toggle
|
||||||
|
id = id or str(randrange(1_000_000))
|
||||||
|
|
||||||
|
def on_hotkey():
|
||||||
|
on_activated()
|
||||||
|
self.web.eval(f'toggleEditorButton("#{id}");')
|
||||||
|
|
||||||
|
else:
|
||||||
|
on_hotkey = on_activated
|
||||||
|
|
||||||
|
QShortcut( # type: ignore
|
||||||
|
QKeySequence(keys),
|
||||||
|
self.widget,
|
||||||
|
activated=on_hotkey,
|
||||||
|
)
|
||||||
|
|
||||||
btn = self._addButton(
|
btn = self._addButton(
|
||||||
icon,
|
icon,
|
||||||
cmd,
|
cmd,
|
||||||
|
@ -114,19 +114,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="3">
|
|
||||||
<widget class="QPushButton" name="previewButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>ACTIONS_PREVIEW</string>
|
|
||||||
</property>
|
|
||||||
<property name="shortcut">
|
|
||||||
<string notr="true">Ctrl+Shift+P</string>
|
|
||||||
</property>
|
|
||||||
<property name="checkable">
|
|
||||||
<bool>true</bool>
|
|
||||||
</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">
|
||||||
|
@ -16,6 +16,7 @@ from aqt.qt import (
|
|||||||
QIcon,
|
QIcon,
|
||||||
QKeySequence,
|
QKeySequence,
|
||||||
QPixmap,
|
QPixmap,
|
||||||
|
QShortcut,
|
||||||
Qt,
|
Qt,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
@ -63,6 +64,9 @@ class Previewer(QDialog):
|
|||||||
def _create_gui(self):
|
def _create_gui(self):
|
||||||
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
|
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
|
||||||
|
|
||||||
|
self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
|
||||||
|
qconnect(self.close_shortcut.activated, self.close)
|
||||||
|
|
||||||
qconnect(self.finished, self._on_finished)
|
qconnect(self.finished, self._on_finished)
|
||||||
self.silentlyClose = True
|
self.silentlyClose = True
|
||||||
self.vbox = QVBoxLayout()
|
self.vbox = QVBoxLayout()
|
||||||
@ -305,10 +309,6 @@ class BrowserPreviewer(MultiCardPreviewer):
|
|||||||
self._last_card_id = c.id
|
self._last_card_id = c.id
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _on_finished(self, ok):
|
|
||||||
super()._on_finished(ok)
|
|
||||||
self._parent.form.previewButton.setChecked(False)
|
|
||||||
|
|
||||||
def _on_prev_card(self):
|
def _on_prev_card(self):
|
||||||
self._parent.editor.saveNow(
|
self._parent.editor.saveNow(
|
||||||
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
||||||
|
@ -136,7 +136,7 @@ class VideoPlayer(Player): # pylint: disable=abstract-method
|
|||||||
|
|
||||||
class AVPlayer:
|
class AVPlayer:
|
||||||
players: List[Player] = []
|
players: List[Player] = []
|
||||||
# when a new batch of audio is played, shoud the currently playing
|
# when a new batch of audio is played, should the currently playing
|
||||||
# audio be stopped?
|
# audio be stopped?
|
||||||
interrupt_current_audio = True
|
interrupt_current_audio = True
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ service BackendService {
|
|||||||
|
|
||||||
// searching
|
// searching
|
||||||
|
|
||||||
|
rpc FilterToSearch(FilterToSearchIn) 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);
|
||||||
@ -758,6 +759,35 @@ message BuiltinSearchOrder {
|
|||||||
bool reverse = 2;
|
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;
|
||||||
|
}
|
||||||
|
oneof filter {
|
||||||
|
NamedFilter name = 1;
|
||||||
|
string tag = 2;
|
||||||
|
string deck = 3;
|
||||||
|
string note = 4;
|
||||||
|
uint32 template = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message ConcatenateSearchesIn {
|
message ConcatenateSearchesIn {
|
||||||
enum Separator {
|
enum Separator {
|
||||||
AND = 0;
|
AND = 0;
|
||||||
|
@ -14,7 +14,7 @@ for path in sys.argv[2:]:
|
|||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
if orig != new:
|
if orig != new:
|
||||||
if want_fix:
|
if want_fix:
|
||||||
with open(os.path.join(workspace, path), "w") as file:
|
with open(os.path.join(workspace, path), "w", newline="\n") as file:
|
||||||
file.write(new)
|
file.write(new)
|
||||||
print("fixed", path)
|
print("fixed", path)
|
||||||
else:
|
else:
|
||||||
|
@ -36,8 +36,8 @@ use crate::{
|
|||||||
sched::new::NewCardSortOrder,
|
sched::new::NewCardSortOrder,
|
||||||
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, BoolSeparator,
|
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||||
SortMode,
|
BoolSeparator, Node, SearchNode, SortMode, StateKind, TemplateKind,
|
||||||
},
|
},
|
||||||
stats::studied_today,
|
stats::studied_today,
|
||||||
sync::{
|
sync::{
|
||||||
@ -45,7 +45,7 @@ use crate::{
|
|||||||
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
||||||
},
|
},
|
||||||
template::RenderedNode,
|
template::RenderedNode,
|
||||||
text::{extract_av_tags, strip_av_tags, AVTag},
|
text::{escape_anki_wildcards, extract_av_tags, strip_av_tags, AVTag},
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
types::Usn,
|
||||||
};
|
};
|
||||||
@ -277,6 +277,57 @@ 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: None,
|
||||||
|
}),
|
||||||
|
NamedFilter::AgainToday => Node::Search(SearchNode::Rated {
|
||||||
|
days: 1,
|
||||||
|
ease: Some(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(
|
||||||
|
escape_anki_wildcards(&s).into_owned().into(),
|
||||||
|
)),
|
||||||
|
Filter::Deck(s) => Node::Search(SearchNode::Deck(
|
||||||
|
escape_anki_wildcards(&s).into_owned().into(),
|
||||||
|
)),
|
||||||
|
Filter::Note(s) => Node::Search(SearchNode::NoteType(
|
||||||
|
escape_anki_wildcards(&s).into_owned().into(),
|
||||||
|
)),
|
||||||
|
Filter::Template(u) => {
|
||||||
|
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<BoolSeparatorProto> for BoolSeparator {
|
impl From<BoolSeparatorProto> for BoolSeparator {
|
||||||
fn from(sep: BoolSeparatorProto) -> Self {
|
fn from(sep: BoolSeparatorProto) -> Self {
|
||||||
match sep {
|
match sep {
|
||||||
@ -408,6 +459,10 @@ impl BackendService for Backend {
|
|||||||
// searching
|
// searching
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result<pb::String> {
|
||||||
|
Ok(write_nodes(&[input.into()]).into())
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_search(&self, input: pb::String) -> Result<pb::String> {
|
fn normalize_search(&self, input: pb::String) -> Result<pb::String> {
|
||||||
Ok(normalize_search(&input.val)?.into())
|
Ok(normalize_search(&input.val)?.into())
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ mod sqlwriter;
|
|||||||
mod writer;
|
mod writer;
|
||||||
|
|
||||||
pub use cards::SortMode;
|
pub use cards::SortMode;
|
||||||
|
pub use parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind};
|
||||||
pub use writer::{
|
pub use writer::{
|
||||||
concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator,
|
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||||
|
BoolSeparator,
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@ impl<I> From<nom::Err<(I, ErrorKind)>> for ParseError {
|
|||||||
type ParseResult<T> = std::result::Result<T, ParseError>;
|
type ParseResult<T> = std::result::Result<T, ParseError>;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(super) enum Node<'a> {
|
pub enum Node<'a> {
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
Not(Box<Node<'a>>),
|
Not(Box<Node<'a>>),
|
||||||
@ -51,7 +51,7 @@ pub(super) enum Node<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(super) enum SearchNode<'a> {
|
pub enum SearchNode<'a> {
|
||||||
// text without a colon
|
// text without a colon
|
||||||
UnqualifiedText(Cow<'a, str>),
|
UnqualifiedText(Cow<'a, str>),
|
||||||
// foo:bar, where foo doesn't match a term below
|
// foo:bar, where foo doesn't match a term below
|
||||||
@ -91,7 +91,7 @@ pub(super) enum SearchNode<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(super) enum PropertyKind {
|
pub enum PropertyKind {
|
||||||
Due(i32),
|
Due(i32),
|
||||||
Interval(u32),
|
Interval(u32),
|
||||||
Reps(u32),
|
Reps(u32),
|
||||||
@ -101,7 +101,7 @@ pub(super) enum PropertyKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(super) enum StateKind {
|
pub enum StateKind {
|
||||||
New,
|
New,
|
||||||
Review,
|
Review,
|
||||||
Learning,
|
Learning,
|
||||||
@ -113,7 +113,7 @@ pub(super) enum StateKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(super) enum TemplateKind<'a> {
|
pub enum TemplateKind<'a> {
|
||||||
Ordinal(u16),
|
Ordinal(u16),
|
||||||
Name(Cow<'a, str>),
|
Name(Cow<'a, str>),
|
||||||
}
|
}
|
||||||
@ -273,8 +273,8 @@ fn search_node_for_text_with_argument<'a>(
|
|||||||
val: &'a str,
|
val: &'a str,
|
||||||
) -> ParseResult<SearchNode<'a>> {
|
) -> ParseResult<SearchNode<'a>> {
|
||||||
Ok(match key.to_ascii_lowercase().as_str() {
|
Ok(match key.to_ascii_lowercase().as_str() {
|
||||||
"added" => SearchNode::AddedInDays(val.parse()?),
|
"added" => parse_added(val)?,
|
||||||
"edited" => SearchNode::EditedInDays(val.parse()?),
|
"edited" => parse_edited(val)?,
|
||||||
"deck" => SearchNode::Deck(unescape(val)?),
|
"deck" => SearchNode::Deck(unescape(val)?),
|
||||||
"note" => SearchNode::NoteType(unescape(val)?),
|
"note" => SearchNode::NoteType(unescape(val)?),
|
||||||
"tag" => SearchNode::Tag(unescape(val)?),
|
"tag" => SearchNode::Tag(unescape(val)?),
|
||||||
@ -309,6 +309,20 @@ fn check_id_list(s: &str) -> ParseResult<&str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// eg added:1
|
||||||
|
fn parse_added(s: &str) -> ParseResult<SearchNode<'static>> {
|
||||||
|
let n: u32 = s.parse()?;
|
||||||
|
let days = n.max(1);
|
||||||
|
Ok(SearchNode::AddedInDays(days))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// eg edited:1
|
||||||
|
fn parse_edited(s: &str) -> ParseResult<SearchNode<'static>> {
|
||||||
|
let n: u32 = s.parse()?;
|
||||||
|
let days = n.max(1);
|
||||||
|
Ok(SearchNode::EditedInDays(days))
|
||||||
|
}
|
||||||
|
|
||||||
/// eg is:due
|
/// eg is:due
|
||||||
fn parse_state(s: &str) -> ParseResult<SearchNode<'static>> {
|
fn parse_state(s: &str) -> ParseResult<SearchNode<'static>> {
|
||||||
use StateKind::*;
|
use StateKind::*;
|
||||||
@ -339,7 +353,10 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode<'static>> {
|
|||||||
/// second arg must be between 0-4
|
/// second arg must be between 0-4
|
||||||
fn parse_rated(val: &str) -> ParseResult<SearchNode<'static>> {
|
fn parse_rated(val: &str) -> ParseResult<SearchNode<'static>> {
|
||||||
let mut it = val.splitn(2, ':');
|
let mut it = val.splitn(2, ':');
|
||||||
let days = it.next().unwrap().parse()?;
|
|
||||||
|
let n: u32 = it.next().unwrap().parse()?;
|
||||||
|
let days = n.max(1).min(365);
|
||||||
|
|
||||||
let ease = match it.next() {
|
let ease = match it.next() {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
let n: u8 = v.parse()?;
|
let n: u8 = v.parse()?;
|
||||||
|
@ -213,8 +213,7 @@ impl SqlWriter<'_> {
|
|||||||
|
|
||||||
fn write_rated(&mut self, days: u32, ease: Option<u8>) -> Result<()> {
|
fn write_rated(&mut self, days: u32, ease: Option<u8>) -> Result<()> {
|
||||||
let today_cutoff = self.col.timing_today()?.next_day_at;
|
let today_cutoff = self.col.timing_today()?.next_day_at;
|
||||||
let days = days.min(365) as i64;
|
let target_cutoff_ms = (today_cutoff - 86_400 * i64::from(days)) * 1_000;
|
||||||
let target_cutoff_ms = (today_cutoff - 86_400 * days) * 1_000;
|
|
||||||
write!(
|
write!(
|
||||||
self.sql,
|
self.sql,
|
||||||
"c.id in (select cid from revlog where id>{}",
|
"c.id in (select cid from revlog where id>{}",
|
||||||
@ -634,6 +633,7 @@ mod test {
|
|||||||
s(ctx, "added:3").0,
|
s(ctx, "added:3").0,
|
||||||
format!("(c.id > {})", (timing.next_day_at - (86_400 * 3)) * 1_000)
|
format!("(c.id > {})", (timing.next_day_at - (86_400 * 3)) * 1_000)
|
||||||
);
|
);
|
||||||
|
assert_eq!(s(ctx, "added:0").0, s(ctx, "added:1").0,);
|
||||||
|
|
||||||
// deck
|
// deck
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -725,6 +725,7 @@ mod test {
|
|||||||
(timing.next_day_at - (86_400 * 365)) * 1_000
|
(timing.next_day_at - (86_400 * 365)) * 1_000
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
assert_eq!(s(ctx, "rated:0").0, s(ctx, "rated:1").0);
|
||||||
|
|
||||||
// props
|
// props
|
||||||
assert_eq!(s(ctx, "prop:lapses=3").0, "(lapses = 3)".to_string());
|
assert_eq!(s(ctx, "prop:lapses=3").0, "(lapses = 3)".to_string());
|
||||||
|
@ -87,7 +87,7 @@ pub fn replace_search_term(search: &str, replacement: &str) -> Result<String> {
|
|||||||
Ok(write_nodes(&nodes))
|
Ok(write_nodes(&nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_nodes<'a, I>(nodes: I) -> String
|
pub fn write_nodes<'a, I>(nodes: I) -> String
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = &'a Node<'a>>,
|
I: IntoIterator<Item = &'a Node<'a>>,
|
||||||
{
|
{
|
||||||
@ -149,7 +149,7 @@ fn write_single_field(field: &str, text: &str, is_re: bool) -> String {
|
|||||||
|
|
||||||
fn write_template(template: &TemplateKind) -> String {
|
fn write_template(template: &TemplateKind) -> String {
|
||||||
match template {
|
match template {
|
||||||
TemplateKind::Ordinal(u) => format!("\"card:{}\"", u),
|
TemplateKind::Ordinal(u) => format!("\"card:{}\"", u + 1),
|
||||||
TemplateKind::Name(s) => format!("\"card:{}\"", s),
|
TemplateKind::Name(s) => format!("\"card:{}\"", s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,6 +323,14 @@ pub(crate) fn to_text(txt: &str) -> Cow<str> {
|
|||||||
RE.replace_all(&txt, "$1")
|
RE.replace_all(&txt, "$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escape Anki wildcards and the backslash for escaping them: \*_
|
||||||
|
pub(crate) fn escape_anki_wildcards(txt: &str) -> Cow<str> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: Regex = Regex::new(r"[\\*_]").unwrap();
|
||||||
|
}
|
||||||
|
RE.replace_all(&txt, r"\$0")
|
||||||
|
}
|
||||||
|
|
||||||
/// Compare text with a possible glob, folding case.
|
/// Compare text with a possible glob, folding case.
|
||||||
pub(crate) fn matches_glob(text: &str, search: &str) -> bool {
|
pub(crate) fn matches_glob(text: &str, search: &str) -> bool {
|
||||||
if is_glob(search) {
|
if is_glob(search) {
|
||||||
|
Loading…
Reference in New Issue
Block a user