mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -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-all-selected = All selected
|
||||
actions-any-selected = Any selected
|
||||
actions-blue-flag = Blue Flag
|
||||
actions-cancel = Cancel
|
||||
actions-choose = Choose
|
||||
|
@ -30,6 +32,7 @@ actions-replay-audio = Replay Audio
|
|||
actions-reposition = Reposition
|
||||
actions-save = Save
|
||||
actions-search = Search
|
||||
actions-select = Select
|
||||
actions-shortcut-key = Shortcut key: { $val }
|
||||
actions-suspend-card = Suspend Card
|
||||
actions-set-due-date = Set Due Date
|
||||
|
|
|
@ -14,6 +14,11 @@ browsing-card = Card
|
|||
browsing-card-list = Card List
|
||||
browsing-card-state = Card State
|
||||
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-deck2 = Change Deck...
|
||||
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-clear-unused = Clear Unused
|
||||
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-ctrlandshiftande = Ctrl+Shift+E
|
||||
browsing-current-deck = Current Deck
|
||||
|
@ -70,14 +76,11 @@ browsing-question = Question
|
|||
browsing-queue-bottom = Queue bottom: { $val }
|
||||
browsing-queue-top = Queue top: { $val }
|
||||
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-replace-with = <b>Replace With</b>:
|
||||
browsing-reposition = Reposition...
|
||||
browsing-reposition-new-cards = Reposition New Cards
|
||||
browsing-reschedule = Reschedule
|
||||
browsing-save-current-filter = Save Current Filter...
|
||||
browsing-search-bar-hint = Search cards/notes (type text, then press Enter)
|
||||
browsing-search-in = Search in:
|
||||
browsing-search-within-formatting-slow = Search within formatting (slow)
|
||||
|
@ -112,7 +115,14 @@ browsing-note-deleted =
|
|||
[one] { $count } note 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-sidebar-expand = Expand
|
||||
browsing-sidebar-collapse = Collapse
|
||||
browsing-sidebar-expand-children = Expand Children
|
||||
browsing-sidebar-collapse-children = Collapse Children
|
||||
browsing-sidebar-decks = Decks
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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-cards-selected-by = cards selected by
|
||||
decks-create-deck = Create Deck
|
||||
|
@ -32,8 +31,3 @@ decks-study = Study
|
|||
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-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
|
||||
from anki.consts import *
|
||||
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
|
||||
DeckTreeNode = _pb.DeckTreeNode
|
||||
|
@ -130,12 +130,16 @@ class DeckManager:
|
|||
|
||||
return deck["id"]
|
||||
|
||||
@legacy_func(sub="remove")
|
||||
def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None:
|
||||
"Remove the deck. If cardsToo, delete any cards inside."
|
||||
if isinstance(did, str):
|
||||
did = int(did)
|
||||
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(
|
||||
self, skip_empty_default: bool = False, include_filtered: bool = True
|
||||
|
@ -212,10 +216,15 @@ class DeckManager:
|
|||
def count(self) -> int:
|
||||
return len(self.all_names_and_ids())
|
||||
|
||||
def card_count(self, did: int, include_subdecks: bool) -> Any:
|
||||
dids: List[int] = [did]
|
||||
def card_count(
|
||||
self, dids: Union[int, Iterable[int]], include_subdecks: bool
|
||||
) -> Any:
|
||||
if isinstance(dids, int):
|
||||
dids = {dids}
|
||||
else:
|
||||
dids = set(dids)
|
||||
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(
|
||||
"select count() from cards where did in {0} or "
|
||||
"odid in {0}".format(ids2str(dids))
|
||||
|
|
|
@ -18,7 +18,7 @@ import traceback
|
|||
from contextlib import contextmanager
|
||||
from hashlib import sha1
|
||||
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
|
||||
|
||||
|
@ -372,3 +372,26 @@ def pointVersion() -> int:
|
|||
from anki.buildinfo import version
|
||||
|
||||
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",
|
||||
"余时行",
|
||||
"叶峻峣",
|
||||
"RumovZ",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ from aqt.previewer import BrowserPreviewer as PreviewDialog
|
|||
from aqt.previewer import Previewer
|
||||
from aqt.qt import *
|
||||
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.utils import (
|
||||
TR,
|
||||
|
@ -941,18 +941,20 @@ QTableView {{ gridline-color: {grid} }}
|
|||
self.sidebar = SidebarTreeView(self)
|
||||
self.sidebarTree = self.sidebar # legacy alias
|
||||
dw.setWidget(self.sidebar)
|
||||
self.sidebar.toolbar = toolbar = SidebarToolbar(self.sidebar)
|
||||
self.sidebar.searchBar = searchBar = SidebarSearchBar(self.sidebar)
|
||||
qconnect(
|
||||
self.form.actionSidebarFilter.triggered,
|
||||
self.focusSidebarSearchBar,
|
||||
)
|
||||
l = QVBoxLayout()
|
||||
l.addWidget(searchBar)
|
||||
l.addWidget(self.sidebar)
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.setSpacing(0)
|
||||
grid = QGridLayout()
|
||||
grid.addWidget(searchBar, 0, 0)
|
||||
grid.addWidget(toolbar, 0, 1)
|
||||
grid.addWidget(self.sidebar, 1, 0, 1, 2)
|
||||
grid.setContentsMargins(0, 0, 0, 0)
|
||||
grid.setSpacing(0)
|
||||
w = QWidget()
|
||||
w.setLayout(l)
|
||||
w.setLayout(grid)
|
||||
dw.setWidget(w)
|
||||
self.sidebarDockWidget.setFloating(False)
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from aqt.utils import (
|
|||
shortcut,
|
||||
showInfo,
|
||||
showWarning,
|
||||
tooltip,
|
||||
tr,
|
||||
)
|
||||
|
||||
|
@ -303,32 +304,14 @@ class DeckBrowser:
|
|||
|
||||
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:
|
||||
if self.ask_delete_deck(did):
|
||||
|
||||
def do_delete() -> None:
|
||||
return self.mw.col.decks.rem(did, True)
|
||||
def do_delete() -> int:
|
||||
return self.mw.col.decks.remove([did])
|
||||
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.update_undo_actions()
|
||||
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)
|
||||
|
||||
|
|
|
@ -10,5 +10,7 @@
|
|||
<file>icons/clock.svg</file>
|
||||
<file>icons/card-state.svg</file>
|
||||
<file>icons/flag.svg</file>
|
||||
<file>icons/select.svg</file>
|
||||
<file>icons/magnifying_glass.svg</file>
|
||||
</qresource>
|
||||
</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;
|
||||
}
|
||||
|
||||
message DeckIDs {
|
||||
repeated int64 dids = 1;
|
||||
}
|
||||
|
||||
message DeckConfigID {
|
||||
int64 dcid = 1;
|
||||
}
|
||||
|
@ -130,7 +134,7 @@ service DecksService {
|
|||
rpc GetDeckLegacy(DeckID) returns (Json);
|
||||
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
|
||||
rpc NewDeckLegacy(Bool) returns (Json);
|
||||
rpc RemoveDeck(DeckID) returns (Empty);
|
||||
rpc RemoveDecks(DeckIDs) returns (UInt32);
|
||||
rpc DragDropDecks(DragDropDecksIn) returns (Empty);
|
||||
rpc RenameDeck(RenameDeckIn) returns (Empty);
|
||||
}
|
||||
|
@ -210,6 +214,7 @@ service DeckConfigService {
|
|||
service TagsService {
|
||||
rpc ClearUnusedTags(Empty) returns (Empty);
|
||||
rpc AllTags(Empty) returns (StringList);
|
||||
rpc ExpungeTags(String) returns (UInt32);
|
||||
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
||||
rpc ClearTag(String) returns (Empty);
|
||||
rpc TagTree(Empty) returns (TagTreeNode);
|
||||
|
|
|
@ -109,8 +109,8 @@ impl DecksService for Backend {
|
|||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn remove_deck(&self, input: pb::DeckId) -> Result<pb::Empty> {
|
||||
self.with_col(|col| col.remove_deck_and_child_decks(input.into()))
|
||||
fn remove_decks(&self, input: pb::DeckIDs) -> Result<pb::UInt32> {
|
||||
self.with_col(|col| col.remove_decks_and_child_decks(&Into::<Vec<DeckID>>::into(input)))
|
||||
.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 {
|
||||
fn from(did: DeckID) -> Self {
|
||||
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 {
|
||||
fn from(_val: ()) -> Self {
|
||||
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> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
|
|
|
@ -466,44 +466,53 @@ impl Collection {
|
|||
self.storage.get_deck_id(&machine_name)
|
||||
}
|
||||
|
||||
pub fn remove_deck_and_child_decks(&mut self, did: DeckID) -> Result<()> {
|
||||
self.transact(Some(UndoableOpKind::RemoveDeck), |col| {
|
||||
pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckID]) -> Result<usize> {
|
||||
let mut card_count = 0;
|
||||
self.transact(None, |col| {
|
||||
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)?;
|
||||
|
||||
// top level
|
||||
col.remove_single_deck(&deck, usn)?;
|
||||
card_count += col.remove_single_deck(&deck, usn)?;
|
||||
|
||||
// remove children
|
||||
for deck in child_decks {
|
||||
col.remove_single_deck(&deck, usn)?;
|
||||
card_count += col.remove_single_deck(&deck, usn)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
Ok(card_count)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<()> {
|
||||
match deck.kind {
|
||||
pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<usize> {
|
||||
let card_count = match deck.kind {
|
||||
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)?;
|
||||
if deck.id.0 == 1 {
|
||||
// if deleting the default deck, ensure there's a new one, and avoid the grave
|
||||
let mut deck = deck.to_owned();
|
||||
deck.name = self.i18n.tr(TR::DeckConfigDefaultName).into();
|
||||
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 {
|
||||
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)?;
|
||||
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)>> {
|
||||
|
@ -820,7 +829,7 @@ mod test {
|
|||
|
||||
// delete top level
|
||||
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
|
||||
assert_eq!(sorted_names(&col), vec!["default", "Default+"]);
|
||||
|
|
|
@ -488,3 +488,11 @@ impl From<ParseIntError> for AnkiError {
|
|||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut changed = false;
|
||||
for tag in &mut self.tags {
|
||||
|
|
|
@ -90,6 +90,15 @@ impl SqliteStorage {
|
|||
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<()> {
|
||||
self.db
|
||||
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
||||
|
|
|
@ -1532,7 +1532,7 @@ mod test {
|
|||
col1.remove_cards_and_orphaned_notes(&[cardid])?;
|
||||
let usn = col1.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;
|
||||
assert_eq!(out.required, SyncActionRequired::NoChanges);
|
||||
|
|
|
@ -292,6 +292,37 @@ impl Collection {
|
|||
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<()> {
|
||||
let mut name = name;
|
||||
let tag;
|
||||
|
|
Loading…
Reference in a new issue