Merge branch 'master' of https://github.com/ankitects/anki into tagtree

This commit is contained in:
abdo 2021-01-12 23:31:58 +03:00
commit 72e8f9d640
17 changed files with 299 additions and 129 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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%;
} }

View File

@ -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,

View File

@ -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">

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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:

View File

@ -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())
} }

View File

@ -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,
}; };

View File

@ -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()?;

View File

@ -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());

View File

@ -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),
} }
} }

View File

@ -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) {