mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 15:02:21 -04:00
Merge pull request #1044 from RumovZ/sidebar-tools
Add sidebar modes for different click behaviour
This commit is contained in:
commit
71789eb51a
22 changed files with 877 additions and 315 deletions
|
@ -1,4 +1,6 @@
|
||||||
actions-add = Add
|
actions-add = Add
|
||||||
|
actions-all-selected = All selected
|
||||||
|
actions-any-selected = Any selected
|
||||||
actions-blue-flag = Blue Flag
|
actions-blue-flag = Blue Flag
|
||||||
actions-cancel = Cancel
|
actions-cancel = Cancel
|
||||||
actions-choose = Choose
|
actions-choose = Choose
|
||||||
|
@ -30,6 +32,7 @@ actions-replay-audio = Replay Audio
|
||||||
actions-reposition = Reposition
|
actions-reposition = Reposition
|
||||||
actions-save = Save
|
actions-save = Save
|
||||||
actions-search = Search
|
actions-search = Search
|
||||||
|
actions-select = Select
|
||||||
actions-shortcut-key = Shortcut key: { $val }
|
actions-shortcut-key = Shortcut key: { $val }
|
||||||
actions-suspend-card = Suspend Card
|
actions-suspend-card = Suspend Card
|
||||||
actions-set-due-date = Set Due Date
|
actions-set-due-date = Set Due Date
|
||||||
|
|
|
@ -14,6 +14,11 @@ browsing-card = Card
|
||||||
browsing-card-list = Card List
|
browsing-card-list = Card List
|
||||||
browsing-card-state = Card State
|
browsing-card-state = Card State
|
||||||
browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck.
|
browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck.
|
||||||
|
browsing-cards-deleted =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } card deleted.
|
||||||
|
*[other] { $count } cards deleted.
|
||||||
|
}
|
||||||
browsing-change-deck = Change Deck
|
browsing-change-deck = Change Deck
|
||||||
browsing-change-deck2 = Change Deck...
|
browsing-change-deck2 = Change Deck...
|
||||||
browsing-change-note-type = Change Note Type
|
browsing-change-note-type = Change Note Type
|
||||||
|
@ -21,6 +26,7 @@ browsing-change-note-type2 = Change Note Type...
|
||||||
browsing-change-to = Change { $val } to:
|
browsing-change-to = Change { $val } to:
|
||||||
browsing-clear-unused = Clear Unused
|
browsing-clear-unused = Clear Unused
|
||||||
browsing-clear-unused-tags = Clear Unused Tags
|
browsing-clear-unused-tags = Clear Unused Tags
|
||||||
|
browsing-confirm-saved-search-overwrite = A saved search with the name { $name } already exists. Do you want to overwrite it?
|
||||||
browsing-created = Created
|
browsing-created = Created
|
||||||
browsing-ctrlandshiftande = Ctrl+Shift+E
|
browsing-ctrlandshiftande = Ctrl+Shift+E
|
||||||
browsing-current-deck = Current Deck
|
browsing-current-deck = Current Deck
|
||||||
|
@ -70,14 +76,11 @@ browsing-question = Question
|
||||||
browsing-queue-bottom = Queue bottom: { $val }
|
browsing-queue-bottom = Queue bottom: { $val }
|
||||||
browsing-queue-top = Queue top: { $val }
|
browsing-queue-top = Queue top: { $val }
|
||||||
browsing-randomize-order = Randomize order
|
browsing-randomize-order = Randomize order
|
||||||
browsing-remove-current-filter = Remove Current Filter...
|
|
||||||
browsing-remove-from-your-saved-searches = Remove { $val } from your saved searches?
|
|
||||||
browsing-remove-tags = Remove Tags...
|
browsing-remove-tags = Remove Tags...
|
||||||
browsing-replace-with = <b>Replace With</b>:
|
browsing-replace-with = <b>Replace With</b>:
|
||||||
browsing-reposition = Reposition...
|
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-search-bar-hint = Search cards/notes (type text, then press Enter)
|
browsing-search-bar-hint = Search cards/notes (type text, then press Enter)
|
||||||
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)
|
||||||
|
@ -112,7 +115,14 @@ browsing-note-deleted =
|
||||||
[one] { $count } note deleted.
|
[one] { $count } note deleted.
|
||||||
*[other] { $count } notes deleted.
|
*[other] { $count } notes deleted.
|
||||||
}
|
}
|
||||||
|
browsing-notes-updated =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } note updated.
|
||||||
|
*[other] { $count } notes updated.
|
||||||
|
}
|
||||||
browsing-window-title = Browse ({ $selected } of { $total } cards selected)
|
browsing-window-title = Browse ({ $selected } of { $total } cards selected)
|
||||||
|
browsing-sidebar-expand = Expand
|
||||||
|
browsing-sidebar-collapse = Collapse
|
||||||
browsing-sidebar-expand-children = Expand Children
|
browsing-sidebar-expand-children = Expand Children
|
||||||
browsing-sidebar-collapse-children = Collapse Children
|
browsing-sidebar-collapse-children = Collapse Children
|
||||||
browsing-sidebar-decks = Decks
|
browsing-sidebar-decks = Decks
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N)
|
decks-add-new-deck-ctrlandn = Add New Deck (Ctrl+N)
|
||||||
decks-are-you-sure-you-wish-to = Are you sure you wish to delete { $val }?
|
|
||||||
decks-build = Build
|
decks-build = Build
|
||||||
decks-cards-selected-by = cards selected by
|
decks-cards-selected-by = cards selected by
|
||||||
decks-create-deck = Create Deck
|
decks-create-deck = Create Deck
|
||||||
|
@ -32,8 +31,3 @@ decks-study = Study
|
||||||
decks-study-deck = Study Deck
|
decks-study-deck = Study Deck
|
||||||
decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it?
|
decks-the-provided-search-did-not-match = The provided search did not match any cards. Would you like to revise it?
|
||||||
decks-unmovable-cards = Show any excluded cards
|
decks-unmovable-cards = Show any excluded cards
|
||||||
decks-it-has-card =
|
|
||||||
{ $count ->
|
|
||||||
[one] It has { $count } card.
|
|
||||||
*[other] It has { $count } cards.
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import anki # pylint: disable=unused-import
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes
|
from anki.utils import from_json_bytes, ids2str, intTime, legacy_func, to_json_bytes
|
||||||
|
|
||||||
# public exports
|
# public exports
|
||||||
DeckTreeNode = _pb.DeckTreeNode
|
DeckTreeNode = _pb.DeckTreeNode
|
||||||
|
@ -130,12 +130,16 @@ class DeckManager:
|
||||||
|
|
||||||
return deck["id"]
|
return deck["id"]
|
||||||
|
|
||||||
|
@legacy_func(sub="remove")
|
||||||
def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None:
|
def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None:
|
||||||
"Remove the deck. If cardsToo, delete any cards inside."
|
"Remove the deck. If cardsToo, delete any cards inside."
|
||||||
if isinstance(did, str):
|
if isinstance(did, str):
|
||||||
did = int(did)
|
did = int(did)
|
||||||
assert cardsToo and childrenToo
|
assert cardsToo and childrenToo
|
||||||
self.col._backend.remove_deck(did)
|
self.remove([did])
|
||||||
|
|
||||||
|
def remove(self, dids: List[int]) -> int:
|
||||||
|
return self.col._backend.remove_decks(dids)
|
||||||
|
|
||||||
def all_names_and_ids(
|
def all_names_and_ids(
|
||||||
self, skip_empty_default: bool = False, include_filtered: bool = True
|
self, skip_empty_default: bool = False, include_filtered: bool = True
|
||||||
|
@ -212,10 +216,15 @@ class DeckManager:
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
return len(self.all_names_and_ids())
|
return len(self.all_names_and_ids())
|
||||||
|
|
||||||
def card_count(self, did: int, include_subdecks: bool) -> Any:
|
def card_count(
|
||||||
dids: List[int] = [did]
|
self, dids: Union[int, Iterable[int]], include_subdecks: bool
|
||||||
|
) -> Any:
|
||||||
|
if isinstance(dids, int):
|
||||||
|
dids = {dids}
|
||||||
|
else:
|
||||||
|
dids = set(dids)
|
||||||
if include_subdecks:
|
if include_subdecks:
|
||||||
dids += [r[1] for r in self.children(did)]
|
dids.update([child[1] for did in dids for child in self.children(did)])
|
||||||
count = self.col.db.scalar(
|
count = self.col.db.scalar(
|
||||||
"select count() from cards where did in {0} or "
|
"select count() from cards where did in {0} or "
|
||||||
"odid in {0}".format(ids2str(dids))
|
"odid in {0}".format(ids2str(dids))
|
||||||
|
|
|
@ -18,7 +18,7 @@ import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from html.entities import name2codepoint
|
from html.entities import name2codepoint
|
||||||
from typing import Any, Iterable, Iterator, List, Match, Optional, Union
|
from typing import Any, Callable, Iterable, Iterator, List, Match, Optional, Union
|
||||||
|
|
||||||
from anki.dbproxy import DBProxy
|
from anki.dbproxy import DBProxy
|
||||||
|
|
||||||
|
@ -372,3 +372,26 @@ def pointVersion() -> int:
|
||||||
from anki.buildinfo import version
|
from anki.buildinfo import version
|
||||||
|
|
||||||
return int(version.split(".")[-1])
|
return int(version.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
# Legacy support
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_func(sub: Optional[str] = None) -> Callable:
|
||||||
|
"""Print a deprecation warning for the decorated callable recommending the use of
|
||||||
|
'sub' instead, if provided.
|
||||||
|
"""
|
||||||
|
if sub:
|
||||||
|
hint = f", use '{sub}' instead"
|
||||||
|
else:
|
||||||
|
hint = ""
|
||||||
|
|
||||||
|
def decorater(func: Callable) -> Callable:
|
||||||
|
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
print(f"'{func.__name__}' is deprecated{hint}.")
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_func
|
||||||
|
|
||||||
|
return decorater
|
||||||
|
|
|
@ -205,6 +205,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
"Gustavo Costa",
|
"Gustavo Costa",
|
||||||
"余时行",
|
"余时行",
|
||||||
"叶峻峣",
|
"叶峻峣",
|
||||||
|
"RumovZ",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ from aqt.previewer import BrowserPreviewer as PreviewDialog
|
||||||
from aqt.previewer import Previewer
|
from aqt.previewer import Previewer
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.scheduling import forget_cards, set_due_date_dialog
|
from aqt.scheduling import forget_cards, set_due_date_dialog
|
||||||
from aqt.sidebar import SidebarSearchBar, SidebarTreeView
|
from aqt.sidebar import SidebarSearchBar, SidebarToolbar, SidebarTreeView
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
TR,
|
TR,
|
||||||
|
@ -941,18 +941,20 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
self.sidebar = SidebarTreeView(self)
|
self.sidebar = SidebarTreeView(self)
|
||||||
self.sidebarTree = self.sidebar # legacy alias
|
self.sidebarTree = self.sidebar # legacy alias
|
||||||
dw.setWidget(self.sidebar)
|
dw.setWidget(self.sidebar)
|
||||||
|
self.sidebar.toolbar = toolbar = SidebarToolbar(self.sidebar)
|
||||||
self.sidebar.searchBar = searchBar = SidebarSearchBar(self.sidebar)
|
self.sidebar.searchBar = searchBar = SidebarSearchBar(self.sidebar)
|
||||||
qconnect(
|
qconnect(
|
||||||
self.form.actionSidebarFilter.triggered,
|
self.form.actionSidebarFilter.triggered,
|
||||||
self.focusSidebarSearchBar,
|
self.focusSidebarSearchBar,
|
||||||
)
|
)
|
||||||
l = QVBoxLayout()
|
grid = QGridLayout()
|
||||||
l.addWidget(searchBar)
|
grid.addWidget(searchBar, 0, 0)
|
||||||
l.addWidget(self.sidebar)
|
grid.addWidget(toolbar, 0, 1)
|
||||||
l.setContentsMargins(0, 0, 0, 0)
|
grid.addWidget(self.sidebar, 1, 0, 1, 2)
|
||||||
l.setSpacing(0)
|
grid.setContentsMargins(0, 0, 0, 0)
|
||||||
|
grid.setSpacing(0)
|
||||||
w = QWidget()
|
w = QWidget()
|
||||||
w.setLayout(l)
|
w.setLayout(grid)
|
||||||
dw.setWidget(w)
|
dw.setWidget(w)
|
||||||
self.sidebarDockWidget.setFloating(False)
|
self.sidebarDockWidget.setFloating(False)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from aqt.utils import (
|
||||||
shortcut,
|
shortcut,
|
||||||
showInfo,
|
showInfo,
|
||||||
showWarning,
|
showWarning,
|
||||||
|
tooltip,
|
||||||
tr,
|
tr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -303,32 +304,14 @@ class DeckBrowser:
|
||||||
|
|
||||||
self.mw.taskman.with_progress(process, on_done)
|
self.mw.taskman.with_progress(process, on_done)
|
||||||
|
|
||||||
def ask_delete_deck(self, did: int) -> bool:
|
|
||||||
deck = self.mw.col.decks.get(did)
|
|
||||||
if deck["dyn"]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
count = self.mw.col.decks.card_count(did, include_subdecks=True)
|
|
||||||
if not count:
|
|
||||||
return True
|
|
||||||
|
|
||||||
extra = tr(TR.DECKS_IT_HAS_CARD, count=count)
|
|
||||||
if askUser(
|
|
||||||
f"{tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=deck['name'])} {extra}"
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _delete(self, did: int) -> None:
|
def _delete(self, did: int) -> None:
|
||||||
if self.ask_delete_deck(did):
|
def do_delete() -> int:
|
||||||
|
return self.mw.col.decks.remove([did])
|
||||||
def do_delete() -> None:
|
|
||||||
return self.mw.col.decks.rem(did, True)
|
|
||||||
|
|
||||||
def on_done(fut: Future) -> None:
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.update_undo_actions()
|
self.mw.update_undo_actions()
|
||||||
self.show()
|
self.show()
|
||||||
res = fut.result() # Required to check for errors
|
tooltip(tr(TR.BROWSING_CARDS_DELETED, count=fut.result()))
|
||||||
|
|
||||||
self.mw.taskman.with_progress(do_delete, on_done)
|
self.mw.taskman.with_progress(do_delete, on_done)
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,7 @@
|
||||||
<file>icons/clock.svg</file>
|
<file>icons/clock.svg</file>
|
||||||
<file>icons/card-state.svg</file>
|
<file>icons/card-state.svg</file>
|
||||||
<file>icons/flag.svg</file>
|
<file>icons/flag.svg</file>
|
||||||
|
<file>icons/select.svg</file>
|
||||||
|
<file>icons/magnifying_glass.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
84
qt/aqt/forms/icons/magnifying_glass.svg
Normal file
84
qt/aqt/forms/icons/magnifying_glass.svg
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
viewBox="0 0 16.933333 16.933334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||||
|
sodipodi:docname="magnifying_glass.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="8"
|
||||||
|
inkscape:cx="10.039334"
|
||||||
|
inkscape:cy="35.645602"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
gridtolerance="10000"
|
||||||
|
objecttolerance="51"
|
||||||
|
guidetolerance="51"
|
||||||
|
inkscape:snap-global="false">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid833" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.38115;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="path835"
|
||||||
|
cx="5.5429349"
|
||||||
|
cy="5.5176048"
|
||||||
|
r="4.7567849" />
|
||||||
|
<g
|
||||||
|
id="path837"
|
||||||
|
style="opacity:1"
|
||||||
|
transform="translate(0.280633,0.25724692)">
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.1866;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||||
|
d="m 9.270412,9.1682417 5.763677,5.8797903"
|
||||||
|
id="path3348" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.997025;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
|
||||||
|
d="M 8.519367,8.5427531 C 7.9111727,9.1535363 7.8640343,9.5551464 8.1618931,9.8774543 l 5.8029559,6.2792797 c 0.603423,0.638261 1.591613,0.648031 2.206659,0.0218 0.614172,-0.625577 0.623586,-1.648878 0.02103,-2.286493 0,0 -6.025394,-5.3742675 -6.3649177,-5.6724746 C 9.4880962,7.9213592 9.1275613,7.9319698 8.519367,8.5427531 Z"
|
||||||
|
id="path3350"
|
||||||
|
sodipodi:nodetypes="zzccczz" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.3 KiB |
168
qt/aqt/forms/icons/select.svg
Normal file
168
qt/aqt/forms/icons/select.svg
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
viewBox="0 0 16.933333 16.933334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||||
|
sodipodi:docname="select.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="powermask"
|
||||||
|
id="path-effect4000"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
uri="#mask-powermask-path-effect4000"
|
||||||
|
invert="false"
|
||||||
|
hide_mask="false"
|
||||||
|
background="true"
|
||||||
|
background_color="#ffffffff" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="powermask"
|
||||||
|
id="path-effect3981"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
uri="#mask-powermask-path-effect3981"
|
||||||
|
invert="false"
|
||||||
|
hide_mask="false"
|
||||||
|
background="true"
|
||||||
|
background_color="#ffffffff" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="powermask"
|
||||||
|
id="path-effect3966"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
uri="#mask-powermask-path-effect3966"
|
||||||
|
invert="false"
|
||||||
|
hide_mask="false"
|
||||||
|
background="true"
|
||||||
|
background_color="#ffffffff" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="powermask"
|
||||||
|
id="path-effect2895"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
uri="#mask-powermask-path-effect2895"
|
||||||
|
invert="false"
|
||||||
|
hide_mask="false"
|
||||||
|
background="true"
|
||||||
|
background_color="#ffffffff" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient866"
|
||||||
|
osb:paint="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#838799;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop864" />
|
||||||
|
</linearGradient>
|
||||||
|
<marker
|
||||||
|
style="overflow:visible"
|
||||||
|
id="Arrow1Lstart"
|
||||||
|
refX="0.0"
|
||||||
|
refY="0.0"
|
||||||
|
orient="auto"
|
||||||
|
inkscape:stockid="Arrow1Lstart"
|
||||||
|
inkscape:isstock="true">
|
||||||
|
<path
|
||||||
|
transform="scale(0.8) translate(12.5,0)"
|
||||||
|
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
|
||||||
|
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
|
||||||
|
id="path2747" />
|
||||||
|
</marker>
|
||||||
|
<inkscape:perspective
|
||||||
|
sodipodi:type="inkscape:persp3d"
|
||||||
|
inkscape:vp_x="0 : 8.466667 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_z="16.933333 : 8.466667 : 1"
|
||||||
|
inkscape:persp3d-origin="8.4666665 : 5.6444447 : 1"
|
||||||
|
id="perspective2694" />
|
||||||
|
<filter
|
||||||
|
id="mask-powermask-path-effect4000_inverse"
|
||||||
|
inkscape:label="filtermask-powermask-path-effect4000"
|
||||||
|
style="color-interpolation-filters:sRGB"
|
||||||
|
height="100"
|
||||||
|
width="100"
|
||||||
|
x="-50"
|
||||||
|
y="-50">
|
||||||
|
<feColorMatrix
|
||||||
|
id="mask-powermask-path-effect4000_primitive1"
|
||||||
|
values="1"
|
||||||
|
type="saturate"
|
||||||
|
result="fbSourceGraphic" />
|
||||||
|
<feColorMatrix
|
||||||
|
id="mask-powermask-path-effect4000_primitive2"
|
||||||
|
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "
|
||||||
|
in="fbSourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="9.1371454"
|
||||||
|
inkscape:cx="33.803843"
|
||||||
|
inkscape:cy="32.605832"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer2"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid833" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Back"
|
||||||
|
style="display:inline;opacity:0.997">
|
||||||
|
<path
|
||||||
|
id="rect2692"
|
||||||
|
transform="translate(0.26458378,0.26458346)"
|
||||||
|
mask="none"
|
||||||
|
d="m 7.4083329,10.847917 -6.87916626,0 V 0.52916664 H 14.022917 l 0,5.29166656"
|
||||||
|
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.165;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:2.33,2.33;stroke-dashoffset:8.621;stroke-opacity:1;paint-order:normal"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="Front"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
style="display:inline">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:0.997319;stroke:#000000;stroke-width:0.535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 10.433094,5.9254024 v 8.8238546 l 2.129895,-1.217083 1.217083,3.042708 1.521355,-0.608542 -1.217083,-3.042708 h 2.434166 z"
|
||||||
|
id="path2710"
|
||||||
|
sodipodi:nodetypes="cccccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.3 KiB |
File diff suppressed because it is too large
Load diff
|
@ -68,6 +68,10 @@ message DeckID {
|
||||||
int64 did = 1;
|
int64 did = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeckIDs {
|
||||||
|
repeated int64 dids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message DeckConfigID {
|
message DeckConfigID {
|
||||||
int64 dcid = 1;
|
int64 dcid = 1;
|
||||||
}
|
}
|
||||||
|
@ -130,7 +134,7 @@ service DecksService {
|
||||||
rpc GetDeckLegacy(DeckID) returns (Json);
|
rpc GetDeckLegacy(DeckID) returns (Json);
|
||||||
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
|
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
|
||||||
rpc NewDeckLegacy(Bool) returns (Json);
|
rpc NewDeckLegacy(Bool) returns (Json);
|
||||||
rpc RemoveDeck(DeckID) returns (Empty);
|
rpc RemoveDecks(DeckIDs) returns (UInt32);
|
||||||
rpc DragDropDecks(DragDropDecksIn) returns (Empty);
|
rpc DragDropDecks(DragDropDecksIn) returns (Empty);
|
||||||
rpc RenameDeck(RenameDeckIn) returns (Empty);
|
rpc RenameDeck(RenameDeckIn) returns (Empty);
|
||||||
}
|
}
|
||||||
|
@ -210,6 +214,7 @@ service DeckConfigService {
|
||||||
service TagsService {
|
service TagsService {
|
||||||
rpc ClearUnusedTags(Empty) returns (Empty);
|
rpc ClearUnusedTags(Empty) returns (Empty);
|
||||||
rpc AllTags(Empty) returns (StringList);
|
rpc AllTags(Empty) returns (StringList);
|
||||||
|
rpc ExpungeTags(String) returns (UInt32);
|
||||||
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
||||||
rpc ClearTag(String) returns (Empty);
|
rpc ClearTag(String) returns (Empty);
|
||||||
rpc TagTree(Empty) returns (TagTreeNode);
|
rpc TagTree(Empty) returns (TagTreeNode);
|
||||||
|
|
|
@ -109,8 +109,8 @@ impl DecksService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_deck(&self, input: pb::DeckId) -> Result<pb::Empty> {
|
fn remove_decks(&self, input: pb::DeckIDs) -> Result<pb::UInt32> {
|
||||||
self.with_col(|col| col.remove_deck_and_child_decks(input.into()))
|
self.with_col(|col| col.remove_decks_and_child_decks(&Into::<Vec<DeckID>>::into(input)))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +137,12 @@ impl From<pb::DeckId> for DeckID {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<pb::DeckIDs> for Vec<DeckID> {
|
||||||
|
fn from(dids: pb::DeckIDs) -> Self {
|
||||||
|
dids.dids.into_iter().map(DeckID).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<DeckID> for pb::DeckId {
|
impl From<DeckID> for pb::DeckId {
|
||||||
fn from(did: DeckID) -> Self {
|
fn from(did: DeckID) -> Self {
|
||||||
pb::DeckId { did: did.0 }
|
pb::DeckId { did: did.0 }
|
||||||
|
|
|
@ -33,6 +33,12 @@ impl From<u32> for pb::UInt32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<usize> for pb::UInt32 {
|
||||||
|
fn from(val: usize) -> Self {
|
||||||
|
pb::UInt32 { val: val as u32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<()> for pb::Empty {
|
impl From<()> for pb::Empty {
|
||||||
fn from(_val: ()) -> Self {
|
fn from(_val: ()) -> Self {
|
||||||
pb::Empty {}
|
pb::Empty {}
|
||||||
|
|
|
@ -23,6 +23,10 @@ impl TagsService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expunge_tags(&self, tags: pb::String) -> Result<pb::UInt32> {
|
||||||
|
self.with_col(|col| col.expunge_tags(tags.val.as_str()).map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result<pb::Empty> {
|
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result<pb::Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
|
|
|
@ -466,44 +466,53 @@ impl Collection {
|
||||||
self.storage.get_deck_id(&machine_name)
|
self.storage.get_deck_id(&machine_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_deck_and_child_decks(&mut self, did: DeckID) -> Result<()> {
|
pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckID]) -> Result<usize> {
|
||||||
self.transact(Some(UndoableOpKind::RemoveDeck), |col| {
|
let mut card_count = 0;
|
||||||
|
self.transact(None, |col| {
|
||||||
let usn = col.usn()?;
|
let usn = col.usn()?;
|
||||||
if let Some(deck) = col.storage.get_deck(did)? {
|
for did in dids {
|
||||||
|
if let Some(deck) = col.storage.get_deck(*did)? {
|
||||||
let child_decks = col.storage.child_decks(&deck)?;
|
let child_decks = col.storage.child_decks(&deck)?;
|
||||||
|
|
||||||
// top level
|
// top level
|
||||||
col.remove_single_deck(&deck, usn)?;
|
card_count += col.remove_single_deck(&deck, usn)?;
|
||||||
|
|
||||||
// remove children
|
// remove children
|
||||||
for deck in child_decks {
|
for deck in child_decks {
|
||||||
col.remove_single_deck(&deck, usn)?;
|
card_count += col.remove_single_deck(&deck, usn)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})?;
|
||||||
|
Ok(card_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<()> {
|
pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<usize> {
|
||||||
match deck.kind {
|
let card_count = match deck.kind {
|
||||||
DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?,
|
DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?,
|
||||||
DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?,
|
DeckKind::Filtered(_) => {
|
||||||
|
self.return_all_cards_in_filtered_deck(deck.id)?;
|
||||||
|
0
|
||||||
}
|
}
|
||||||
|
};
|
||||||
self.clear_aux_config_for_deck(deck.id)?;
|
self.clear_aux_config_for_deck(deck.id)?;
|
||||||
if deck.id.0 == 1 {
|
if deck.id.0 == 1 {
|
||||||
// if deleting the default deck, ensure there's a new one, and avoid the grave
|
// if deleting the default deck, ensure there's a new one, and avoid the grave
|
||||||
let mut deck = deck.to_owned();
|
let mut deck = deck.to_owned();
|
||||||
deck.name = self.i18n.tr(TR::DeckConfigDefaultName).into();
|
deck.name = self.i18n.tr(TR::DeckConfigDefaultName).into();
|
||||||
deck.set_modified(usn);
|
deck.set_modified(usn);
|
||||||
self.add_or_update_single_deck_with_existing_id(&mut deck, usn)
|
self.add_or_update_single_deck_with_existing_id(&mut deck, usn)?;
|
||||||
} else {
|
} else {
|
||||||
self.remove_deck_and_add_grave_undoable(deck.clone(), usn)
|
self.remove_deck_and_add_grave_undoable(deck.clone(), usn)?;
|
||||||
}
|
}
|
||||||
|
Ok(card_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<()> {
|
fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<usize> {
|
||||||
let cids = self.storage.all_cards_in_single_deck(did)?;
|
let cids = self.storage.all_cards_in_single_deck(did)?;
|
||||||
self.remove_cards_and_orphaned_notes(&cids)
|
self.remove_cards_and_orphaned_notes(&cids)?;
|
||||||
|
Ok(cids.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
|
pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result<Vec<(DeckID, String)>> {
|
||||||
|
@ -820,7 +829,7 @@ mod test {
|
||||||
|
|
||||||
// delete top level
|
// delete top level
|
||||||
let top = col.get_or_create_normal_deck("one")?;
|
let top = col.get_or_create_normal_deck("one")?;
|
||||||
col.remove_deck_and_child_decks(top.id)?;
|
col.remove_decks_and_child_decks(&[top.id])?;
|
||||||
|
|
||||||
// should have come back as "Default+" due to conflict
|
// should have come back as "Default+" due to conflict
|
||||||
assert_eq!(sorted_names(&col), vec!["default", "Default+"]);
|
assert_eq!(sorted_names(&col), vec!["default", "Default+"]);
|
||||||
|
|
|
@ -488,3 +488,11 @@ impl From<ParseIntError> for AnkiError {
|
||||||
AnkiError::ParseNumError
|
AnkiError::ParseNumError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<regex::Error> for AnkiError {
|
||||||
|
fn from(_err: regex::Error) -> Self {
|
||||||
|
AnkiError::InvalidInput {
|
||||||
|
info: "invalid regex".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -191,6 +191,12 @@ impl Note {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_tags(&mut self, re: &Regex) -> bool {
|
||||||
|
let old_len = self.tags.len();
|
||||||
|
self.tags.retain(|tag| !re.is_match(tag));
|
||||||
|
old_len > self.tags.len()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_tags<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
|
pub(crate) fn replace_tags<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for tag in &mut self.tags {
|
for tag in &mut self.tags {
|
||||||
|
|
|
@ -90,6 +90,15 @@ impl SqliteStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all matching tags where tag_group is a regexp group that should not match whitespace.
|
||||||
|
pub(crate) fn clear_tag_group(&self, tag_group: &str) -> Result<()> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("delete from tags where tag regexp ?")?
|
||||||
|
.execute(&[format!("(?i)^{}($|::)", tag_group)])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
||||||
|
|
|
@ -1532,7 +1532,7 @@ mod test {
|
||||||
col1.remove_cards_and_orphaned_notes(&[cardid])?;
|
col1.remove_cards_and_orphaned_notes(&[cardid])?;
|
||||||
let usn = col1.usn()?;
|
let usn = col1.usn()?;
|
||||||
col1.remove_note_only_undoable(noteid, usn)?;
|
col1.remove_note_only_undoable(noteid, usn)?;
|
||||||
col1.remove_deck_and_child_decks(deckid)?;
|
col1.remove_decks_and_child_decks(&[deckid])?;
|
||||||
|
|
||||||
let out = ctx.normal_sync(&mut col1).await;
|
let out = ctx.normal_sync(&mut col1).await;
|
||||||
assert_eq!(out.required, SyncActionRequired::NoChanges);
|
assert_eq!(out.required, SyncActionRequired::NoChanges);
|
||||||
|
|
|
@ -292,6 +292,37 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take tags as a whitespace-separated string and remove them from all notes and the storage.
|
||||||
|
pub fn expunge_tags(&mut self, tags: &str) -> Result<usize> {
|
||||||
|
let tag_group = format!("({})", regex::escape(tags.trim()).replace(' ', "|"));
|
||||||
|
let nids = self.nids_for_tags(&tag_group)?;
|
||||||
|
let re = Regex::new(&format!("(?i)^{}(::.*)?$", tag_group))?;
|
||||||
|
self.transact(None, |col| {
|
||||||
|
col.storage.clear_tag_group(&tag_group)?;
|
||||||
|
col.transform_notes(&nids, |note, _nt| {
|
||||||
|
Ok(TransformNoteOutput {
|
||||||
|
changed: note.remove_tags(&re),
|
||||||
|
generate_cards: false,
|
||||||
|
mark_modified: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take tags as a regexp group, i.e. separated with pipes and wrapped in brackets, and return
|
||||||
|
/// the ids of all notes with one of them.
|
||||||
|
fn nids_for_tags(&mut self, tag_group: &str) -> Result<Vec<NoteID>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.storage
|
||||||
|
.db
|
||||||
|
.prepare("select id from notes where tags regexp ?")?;
|
||||||
|
let args = format!("(?i).* {}(::| ).*", tag_group);
|
||||||
|
let nids = stmt
|
||||||
|
.query_map(&[args], |row| row.get(0))?
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
Ok(nids)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> {
|
pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> {
|
||||||
let mut name = name;
|
let mut name = name;
|
||||||
let tag;
|
let tag;
|
||||||
|
|
Loading…
Reference in a new issue