Merge branch 'master' into backend-columns
This commit is contained in:
commit
fadec3dc5b
10
.bazelrc
10
.bazelrc
@ -28,15 +28,11 @@ build --incompatible_default_to_explicit_init_py
|
||||
|
||||
build:ci --show_timestamps --isatty=0 --color=yes --show_progress_rate_limit=5
|
||||
|
||||
# incrementally compile Anki crates in fastbuild mode, but not release mode.
|
||||
# The worker must be separately enabled; see docs/development.md
|
||||
build -c fastbuild --@rules_rust//worker:include_regex=anki.*|rsbridge
|
||||
build:opt -c opt --@rules_rust//worker:include_regex=no-crates-please
|
||||
build --worker_max_instances=Rustc=HOST_CPUS*0.5
|
||||
# disable incremental compilation in release mode
|
||||
build:opt -c opt --@rules_rust//:experimental_incremental_prefixes=
|
||||
|
||||
# the TypeScript workers on Windows choke when deps are changed while they're
|
||||
# still running, so shut them down at the end of the build. Also fixes issues
|
||||
# with the optional Rust worker.
|
||||
# still running, so shut them down at the end of the build.
|
||||
build:windows --worker_quit_after_build
|
||||
|
||||
try-import %workspace%/user.bazelrc
|
||||
|
3
defs.bzl
3
defs.bzl
@ -9,6 +9,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories")
|
||||
load("@com_github_ali5h_rules_pip//:defs.bzl", "pip_import")
|
||||
load("//pip/pyqt5:defs.bzl", "install_pyqt5")
|
||||
load("//ts/esbuild:esbuild_repo.bzl", "esbuild_dependencies")
|
||||
|
||||
anki_version = "2.1.44"
|
||||
|
||||
@ -52,3 +53,5 @@ def setup_deps():
|
||||
)
|
||||
|
||||
sass_repositories()
|
||||
|
||||
esbuild_dependencies()
|
||||
|
@ -153,7 +153,7 @@ following in your user.bazelrc file to enable incremental compilation
|
||||
when using ./run.
|
||||
|
||||
```
|
||||
build --@rules_rust//worker:cache_root=/path/to/folder/to/store/temp/files
|
||||
build --@rules_rust//:experimental_incremental_base=/home/myuser/bazel/incremental
|
||||
```
|
||||
|
||||
The worker support is experimental, so you may need to remove it in future
|
||||
|
@ -40,3 +40,4 @@ errors-unable-open-collection =
|
||||
Anki was unable to open your collection file. If problems persist after restarting your computer, please use the Open Backup button in the profile manager.
|
||||
|
||||
Debug info:
|
||||
errors-windows-tts-runtime-error = The TTS service failed. Please ensure Windows updates are installed, try restarting your computer, and try using a different voice.
|
||||
|
@ -385,7 +385,7 @@ class Collection:
|
||||
note.id = NoteId(out.note_id)
|
||||
return out.changes
|
||||
|
||||
def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChanges:
|
||||
def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount:
|
||||
hooks.notes_will_be_deleted(self, note_ids)
|
||||
return self._backend.remove_notes(note_ids=note_ids, card_ids=[])
|
||||
|
||||
@ -917,11 +917,6 @@ table.review-log {{ {revlog_style} }}
|
||||
assert_exhaustive(self._undo)
|
||||
assert False
|
||||
|
||||
def op_affects_study_queue(self, changes: OpChanges) -> bool:
|
||||
if changes.kind == changes.SET_CARD_FLAG:
|
||||
return False
|
||||
return changes.card or changes.deck or changes.preference
|
||||
|
||||
def op_made_changes(self, changes: OpChanges) -> bool:
|
||||
for field in changes.DESCRIPTOR.fields:
|
||||
if field.name != "kind":
|
||||
|
@ -447,30 +447,31 @@ class DeckManager:
|
||||
# Deck selection
|
||||
#############################################################
|
||||
|
||||
def active(self) -> List[DeckId]:
|
||||
"The currrently active dids."
|
||||
return self.col.get_config("activeDecks", [1])
|
||||
def get_current(self) -> Deck:
|
||||
return self.col._backend.get_current_deck()
|
||||
|
||||
def selected(self) -> DeckId:
|
||||
"The currently selected did."
|
||||
return DeckId(int(self.col.conf["curDeck"]))
|
||||
def set_current(self, deck: DeckId) -> OpChanges:
|
||||
return self.col._backend.set_current_deck(deck)
|
||||
|
||||
def get_current_id(self) -> DeckId:
|
||||
"The currently selected deck ID."
|
||||
return DeckId(self.get_current().id)
|
||||
|
||||
# legacy
|
||||
|
||||
def current(self) -> DeckDict:
|
||||
return self.get(self.selected())
|
||||
|
||||
def select(self, did: DeckId) -> None:
|
||||
"Select a new branch."
|
||||
# make sure arg is an int; legacy callers may be passing in a string
|
||||
did = DeckId(did)
|
||||
current = self.selected()
|
||||
active = self.deck_and_child_ids(did)
|
||||
if current != did or active != self.active():
|
||||
self.col.conf["curDeck"] = did
|
||||
self.col.conf["activeDecks"] = active
|
||||
self.set_current(did)
|
||||
self.col.reset()
|
||||
|
||||
# don't use this, it will likely go away
|
||||
def update_active(self) -> None:
|
||||
self.select(self.current()["id"])
|
||||
def active(self) -> List[DeckId]:
|
||||
return self.col.sched.active_decks
|
||||
|
||||
selected = get_current_id
|
||||
|
||||
# Parents/children
|
||||
#############################################################
|
||||
@ -518,7 +519,7 @@ class DeckManager:
|
||||
)
|
||||
|
||||
def deck_and_child_ids(self, deck_id: DeckId) -> List[DeckId]:
|
||||
parent_name = self.get_legacy(deck_id)["name"]
|
||||
parent_name = self.col.get_deck(deck_id).name
|
||||
out = [deck_id]
|
||||
out.extend(self.child_ids(parent_name))
|
||||
return out
|
||||
|
@ -16,7 +16,7 @@ from typing import List, Optional, Sequence
|
||||
from anki.cards import CardId
|
||||
from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
|
||||
from anki.decks import DeckConfigDict, DeckId, DeckTreeNode
|
||||
from anki.notes import Note
|
||||
from anki.notes import NoteId
|
||||
from anki.utils import ids2str, intTime
|
||||
|
||||
CongratsInfo = _pb.CongratsInfoOut
|
||||
@ -85,8 +85,9 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||
# fixme: only used by totalRevForCurrentDeck and old deck stats;
|
||||
# schedv2 defines separate version
|
||||
def _deckLimit(self) -> str:
|
||||
self.col.decks.update_active()
|
||||
return ids2str(self.col.decks.active())
|
||||
return ids2str(
|
||||
self.col.decks.deck_and_child_ids(self.col.decks.get_current_id())
|
||||
)
|
||||
|
||||
# Filtered deck handling
|
||||
##########################################################################
|
||||
@ -123,20 +124,31 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||
) -> None:
|
||||
self.col._backend.unbury_cards_in_current_deck(mode)
|
||||
|
||||
def suspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
|
||||
def suspend_cards(self, ids: Sequence[CardId]) -> OpChangesWithCount:
|
||||
return self.col._backend.bury_or_suspend_cards(
|
||||
card_ids=ids, mode=BuryOrSuspend.SUSPEND
|
||||
card_ids=ids, note_ids=[], mode=BuryOrSuspend.SUSPEND
|
||||
)
|
||||
|
||||
def bury_cards(self, ids: Sequence[CardId], manual: bool = True) -> OpChanges:
|
||||
def suspend_notes(self, ids: Sequence[NoteId]) -> OpChangesWithCount:
|
||||
return self.col._backend.bury_or_suspend_cards(
|
||||
card_ids=[], note_ids=ids, mode=BuryOrSuspend.SUSPEND
|
||||
)
|
||||
|
||||
def bury_cards(
|
||||
self, ids: Sequence[CardId], manual: bool = True
|
||||
) -> OpChangesWithCount:
|
||||
if manual:
|
||||
mode = BuryOrSuspend.BURY_USER
|
||||
else:
|
||||
mode = BuryOrSuspend.BURY_SCHED
|
||||
return self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
||||
return self.col._backend.bury_or_suspend_cards(
|
||||
card_ids=ids, note_ids=[], mode=mode
|
||||
)
|
||||
|
||||
def bury_note(self, note: Note) -> None:
|
||||
self.bury_cards(note.card_ids())
|
||||
def bury_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount:
|
||||
return self.col._backend.bury_or_suspend_cards(
|
||||
card_ids=[], note_ids=note_ids, mode=BuryOrSuspend.BURY_USER
|
||||
)
|
||||
|
||||
# Resetting/rescheduling
|
||||
##########################################################################
|
||||
|
@ -33,7 +33,7 @@ class Scheduler(V2):
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
self, col: anki.collection.Collection
|
||||
) -> None:
|
||||
self.col = col.weakref()
|
||||
super().__init__(col)
|
||||
self.queueLimit = 50
|
||||
self.reportLimit = 1000
|
||||
self.dynReportLimit = 99999
|
||||
@ -42,7 +42,6 @@ class Scheduler(V2):
|
||||
self.revCount = 0
|
||||
self.newCount = 0
|
||||
self._haveQueues = False
|
||||
self._updateCutoff()
|
||||
|
||||
def answerCard(self, card: Card, ease: int) -> None:
|
||||
self.col.log()
|
||||
|
@ -48,7 +48,16 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||
self.reps = 0
|
||||
self._haveQueues = False
|
||||
self._lrnCutoff = 0
|
||||
self._updateCutoff()
|
||||
self._active_decks: List[DeckId] = []
|
||||
self._current_deck_id = DeckId(1)
|
||||
|
||||
@property
|
||||
def active_decks(self) -> List[DeckId]:
|
||||
"Caller must make sure to make a copy."
|
||||
return self._active_decks
|
||||
|
||||
def _update_active_decks(self) -> None:
|
||||
self._active_decks = self.col.decks.deck_and_child_ids(self._current_deck_id)
|
||||
|
||||
# Daily cutoff
|
||||
##########################################################################
|
||||
@ -65,8 +74,8 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||
##########################################################################
|
||||
|
||||
def reset(self) -> None:
|
||||
self.col.decks.update_active()
|
||||
self._updateCutoff()
|
||||
self._current_deck_id = self.col.decks.selected()
|
||||
self._update_active_decks()
|
||||
self._reset_counts()
|
||||
self._resetLrn()
|
||||
self._resetRev()
|
||||
@ -74,10 +83,8 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||
self._haveQueues = True
|
||||
|
||||
def _reset_counts(self) -> None:
|
||||
tree = self.deck_due_tree(self.col.decks.selected())
|
||||
node = self.col.decks.find_deck_in_tree(
|
||||
tree, DeckId(int(self.col.conf["curDeck"]))
|
||||
)
|
||||
tree = self.deck_due_tree(self._current_deck_id)
|
||||
node = self.col.decks.find_deck_in_tree(tree, self._current_deck_id)
|
||||
if not node:
|
||||
# current deck points to a missing deck
|
||||
self.newCount = 0
|
||||
|
@ -18,6 +18,7 @@ def test_basic():
|
||||
assert col.decks.id("new deck") == parentId
|
||||
# we start with the default col selected
|
||||
assert col.decks.selected() == 1
|
||||
col.reset()
|
||||
assert col.decks.active() == [1]
|
||||
# we can select a different col
|
||||
col.decks.select(parentId)
|
||||
|
@ -501,7 +501,7 @@ def test_misc():
|
||||
col.addNote(note)
|
||||
c = note.cards()[0]
|
||||
# burying
|
||||
col.sched.bury_note(note)
|
||||
col.sched.bury_notes([note.id])
|
||||
col.reset()
|
||||
assert not col.sched.getCard()
|
||||
col.sched.unbury_cards_in_current_deck()
|
||||
|
@ -208,9 +208,9 @@ class AddCards(QDialog):
|
||||
self._load_new_note(sticky_fields_from=note)
|
||||
gui_hooks.add_cards_did_add_note(note)
|
||||
|
||||
add_note(
|
||||
mw=self.mw, note=note, target_deck_id=target_deck_id, success=on_success
|
||||
)
|
||||
add_note(parent=self, note=note, target_deck_id=target_deck_id).success(
|
||||
on_success
|
||||
).run_in_background()
|
||||
|
||||
def _note_can_be_added(self, note: Note) -> bool:
|
||||
result = note.duplicate_or_empty()
|
||||
|
@ -24,7 +24,6 @@ from aqt.editor import Editor
|
||||
from aqt.exporting import ExportDialog
|
||||
from aqt.find_and_replace import FindAndReplaceDialog
|
||||
from aqt.main import ResetReason
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.operations.card import set_card_deck, set_card_flag
|
||||
from aqt.operations.collection import undo
|
||||
from aqt.operations.note import remove_notes
|
||||
@ -128,12 +127,14 @@ class Browser(QMainWindow):
|
||||
gui_hooks.browser_will_show(self)
|
||||
self.show()
|
||||
|
||||
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
|
||||
def on_operation_did_execute(
|
||||
self, changes: OpChanges, handler: Optional[object]
|
||||
) -> None:
|
||||
focused = current_top_level_widget() == self
|
||||
self.table.op_executed(changes, meta, focused)
|
||||
self.sidebar.op_executed(changes, meta, focused)
|
||||
self.table.op_executed(changes, handler, focused)
|
||||
self.sidebar.op_executed(changes, handler, focused)
|
||||
if changes.note or changes.notetype:
|
||||
if meta.handler is not self.editor:
|
||||
if handler is not self.editor:
|
||||
# fixme: this will leave the splitter shown, but with no current
|
||||
# note being edited
|
||||
note = self.editor.note
|
||||
@ -641,11 +642,7 @@ where id in %s"""
|
||||
self.focusTo = self.editor.currentField
|
||||
self.table.to_next_row()
|
||||
|
||||
remove_notes(
|
||||
mw=self.mw,
|
||||
note_ids=nids,
|
||||
success=lambda _: tooltip(tr.browsing_note_deleted(count=len(nids))),
|
||||
)
|
||||
remove_notes(parent=self, note_ids=nids).run_in_background()
|
||||
|
||||
# legacy
|
||||
|
||||
@ -676,7 +673,7 @@ where id in %s"""
|
||||
return
|
||||
did = self.col.decks.id(ret.name)
|
||||
|
||||
set_card_deck(mw=self.mw, card_ids=cids, deck_id=did)
|
||||
set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background()
|
||||
|
||||
# legacy
|
||||
|
||||
@ -694,13 +691,8 @@ where id in %s"""
|
||||
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())):
|
||||
return
|
||||
add_tags_to_notes(
|
||||
mw=self.mw,
|
||||
note_ids=self.selected_notes(),
|
||||
space_separated_tags=tags,
|
||||
success=lambda out: tooltip(
|
||||
tr.browsing_notes_updated(count=out.count), parent=self
|
||||
),
|
||||
)
|
||||
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
|
||||
).run_in_background(initiator=self)
|
||||
|
||||
@ensure_editor_saved_on_trigger
|
||||
def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None:
|
||||
@ -709,14 +701,10 @@ where id in %s"""
|
||||
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete())
|
||||
):
|
||||
return
|
||||
|
||||
remove_tags_from_notes(
|
||||
mw=self.mw,
|
||||
note_ids=self.selected_notes(),
|
||||
space_separated_tags=tags,
|
||||
success=lambda out: tooltip(
|
||||
tr.browsing_notes_updated(count=out.count), parent=self
|
||||
),
|
||||
)
|
||||
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
|
||||
).run_in_background(initiator=self)
|
||||
|
||||
def _prompt_for_tags(self, prompt: str) -> Optional[str]:
|
||||
(tags, ok) = getTag(self, self.col, prompt)
|
||||
@ -727,7 +715,7 @@ where id in %s"""
|
||||
|
||||
@ensure_editor_saved_on_trigger
|
||||
def clear_unused_tags(self) -> None:
|
||||
clear_unused_tags(mw=self.mw, parent=self)
|
||||
clear_unused_tags(parent=self).run_in_background()
|
||||
|
||||
addTags = add_tags_to_selected_notes
|
||||
deleteTags = remove_tags_from_selected_notes
|
||||
@ -744,9 +732,9 @@ where id in %s"""
|
||||
def suspend_selected_cards(self, checked: bool) -> None:
|
||||
cids = self.selected_cards()
|
||||
if checked:
|
||||
suspend_cards(mw=self.mw, card_ids=cids)
|
||||
suspend_cards(parent=self, card_ids=cids).run_in_background()
|
||||
else:
|
||||
unsuspend_cards(mw=self.mw, card_ids=cids)
|
||||
unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background()
|
||||
|
||||
# Exporting
|
||||
######################################################################
|
||||
@ -768,7 +756,9 @@ where id in %s"""
|
||||
if flag == self.card.user_flag():
|
||||
flag = 0
|
||||
|
||||
set_card_flag(mw=self.mw, card_ids=self.selected_cards(), flag=flag)
|
||||
set_card_flag(
|
||||
parent=self, card_ids=self.selected_cards(), flag=flag
|
||||
).run_in_background()
|
||||
|
||||
def _update_flags_menu(self) -> None:
|
||||
flag = self.card and self.card.user_flag()
|
||||
@ -806,25 +796,23 @@ where id in %s"""
|
||||
return
|
||||
|
||||
reposition_new_cards_dialog(
|
||||
mw=self.mw, parent=self, card_ids=self.selected_cards()
|
||||
)
|
||||
parent=self, card_ids=self.selected_cards()
|
||||
).run_in_background()
|
||||
|
||||
@ensure_editor_saved_on_trigger
|
||||
def set_due_date(self) -> None:
|
||||
set_due_date_dialog(
|
||||
mw=self.mw,
|
||||
parent=self,
|
||||
card_ids=self.selected_cards(),
|
||||
config_key=Config.String.SET_DUE_BROWSER,
|
||||
)
|
||||
).run_in_background()
|
||||
|
||||
@ensure_editor_saved_on_trigger
|
||||
def forget_cards(self) -> None:
|
||||
forget_cards(
|
||||
mw=self.mw,
|
||||
parent=self,
|
||||
card_ids=self.selected_cards(),
|
||||
)
|
||||
).run_in_background()
|
||||
|
||||
# Edit: selection
|
||||
######################################################################
|
||||
@ -867,7 +855,7 @@ where id in %s"""
|
||||
######################################################################
|
||||
|
||||
def undo(self) -> None:
|
||||
undo(mw=self.mw, parent=self)
|
||||
undo(parent=self)
|
||||
|
||||
def onUndoState(self, on: bool) -> None:
|
||||
self.form.actionUndo.setEnabled(on)
|
||||
|
@ -3,6 +3,7 @@ window.MathJax = {
|
||||
displayMath: [["\\[", "\\]"]],
|
||||
processRefs: false,
|
||||
processEnvironments: false,
|
||||
processEscapes: false,
|
||||
packages: {
|
||||
"[+]": ["noerrors", "mhchem"],
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
import aqt
|
||||
from anki.collection import OpChanges
|
||||
@ -17,6 +17,7 @@ from aqt.operations.deck import (
|
||||
remove_decks,
|
||||
rename_deck,
|
||||
reparent_decks,
|
||||
set_current_deck,
|
||||
set_deck_collapsed,
|
||||
)
|
||||
from aqt.qt import *
|
||||
@ -76,8 +77,10 @@ class DeckBrowser:
|
||||
if self._refresh_needed:
|
||||
self.refresh()
|
||||
|
||||
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
|
||||
if self.mw.col.op_affects_study_queue(changes):
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
) -> bool:
|
||||
if changes.study_queues and handler is not self:
|
||||
self._refresh_needed = True
|
||||
|
||||
if focused:
|
||||
@ -94,7 +97,7 @@ class DeckBrowser:
|
||||
else:
|
||||
cmd = url
|
||||
if cmd == "open":
|
||||
self._selDeck(arg)
|
||||
self.set_current_deck(DeckId(int(arg)))
|
||||
elif cmd == "opts":
|
||||
self._showOptions(arg)
|
||||
elif cmd == "shared":
|
||||
@ -117,9 +120,10 @@ class DeckBrowser:
|
||||
self.refresh()
|
||||
return False
|
||||
|
||||
def _selDeck(self, did: str) -> None:
|
||||
self.mw.col.decks.select(DeckId(int(did)))
|
||||
self.mw.onOverview()
|
||||
def set_current_deck(self, deck_id: DeckId) -> None:
|
||||
set_current_deck(parent=self.mw, deck_id=deck_id).success(
|
||||
lambda _: self.mw.onOverview()
|
||||
).run_in_background(initiator=self)
|
||||
|
||||
# HTML generation
|
||||
##########################################################################
|
||||
@ -276,7 +280,9 @@ class DeckBrowser:
|
||||
if not new_name or new_name == deck.name:
|
||||
return
|
||||
else:
|
||||
rename_deck(mw=self.mw, deck_id=did, new_name=new_name)
|
||||
rename_deck(
|
||||
parent=self.mw, deck_id=did, new_name=new_name
|
||||
).run_in_background()
|
||||
|
||||
self.mw.query_op(lambda: self.mw.col.get_deck(did), success=prompt)
|
||||
|
||||
@ -291,18 +297,20 @@ class DeckBrowser:
|
||||
if node:
|
||||
node.collapsed = not node.collapsed
|
||||
set_deck_collapsed(
|
||||
mw=self.mw,
|
||||
parent=self.mw,
|
||||
deck_id=did,
|
||||
collapsed=node.collapsed,
|
||||
scope=DeckCollapseScope.REVIEWER,
|
||||
)
|
||||
).run_in_background()
|
||||
self._renderPage(reuse=True)
|
||||
|
||||
def _handle_drag_and_drop(self, source: DeckId, target: DeckId) -> None:
|
||||
reparent_decks(mw=self.mw, parent=self.mw, deck_ids=[source], new_parent=target)
|
||||
reparent_decks(
|
||||
parent=self.mw, deck_ids=[source], new_parent=target
|
||||
).run_in_background()
|
||||
|
||||
def _delete(self, did: DeckId) -> None:
|
||||
remove_decks(mw=self.mw, parent=self.mw, deck_ids=[did])
|
||||
remove_decks(parent=self.mw, deck_ids=[did]).run_in_background()
|
||||
|
||||
# Top buttons
|
||||
######################################################################
|
||||
@ -333,7 +341,8 @@ class DeckBrowser:
|
||||
openLink(f"{aqt.appShared}decks/")
|
||||
|
||||
def _on_create(self) -> None:
|
||||
add_deck_dialog(mw=self.mw, parent=self.mw)
|
||||
if op := add_deck_dialog(parent=self.mw):
|
||||
op.run_in_background()
|
||||
|
||||
######################################################################
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from typing import Optional
|
||||
|
||||
import aqt.editor
|
||||
from anki.collection import OpChanges
|
||||
from anki.errors import NotFoundError
|
||||
from aqt import gui_hooks
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.qt import *
|
||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
|
||||
|
||||
@ -31,8 +31,10 @@ class EditCurrent(QDialog):
|
||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||
self.show()
|
||||
|
||||
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
|
||||
if changes.editor and meta.handler is not self.editor:
|
||||
def on_operation_did_execute(
|
||||
self, changes: OpChanges, handler: Optional[object]
|
||||
) -> None:
|
||||
if changes.editor and handler is not self.editor:
|
||||
# reload note
|
||||
note = self.editor.note
|
||||
try:
|
||||
|
@ -100,7 +100,7 @@ class Editor:
|
||||
redrawing.
|
||||
|
||||
The editor will cause that hook to be fired when it saves changes. To avoid
|
||||
an unwanted refresh, the parent widget should check if meta.handler
|
||||
an unwanted refresh, the parent widget should check if handler
|
||||
corresponds to this editor instance, and ignore the change if it does.
|
||||
"""
|
||||
|
||||
@ -558,7 +558,9 @@ class Editor:
|
||||
|
||||
def _save_current_note(self) -> None:
|
||||
"Call after note is updated with data from webview."
|
||||
update_note(mw=self.mw, note=self.note, handler=self)
|
||||
update_note(parent=self.widget, note=self.note).run_in_background(
|
||||
initiator=self
|
||||
)
|
||||
|
||||
def fonts(self) -> List[Tuple[str, int, bool]]:
|
||||
return [
|
||||
|
@ -310,7 +310,9 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
|
||||
gui_hooks.filtered_deck_dialog_will_add_or_update_deck(self, self.deck)
|
||||
|
||||
add_or_update_filtered_deck(mw=self.mw, deck=self.deck, success=success)
|
||||
add_or_update_filtered_deck(parent=self, deck=self.deck).success(
|
||||
success
|
||||
).run_in_background()
|
||||
|
||||
# Step load/save
|
||||
########################################################
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence
|
||||
from typing import List, Sequence
|
||||
|
||||
import aqt
|
||||
from anki.notes import NoteId
|
||||
from aqt import AnkiQt, QWidget
|
||||
from aqt.operations.note import find_and_replace
|
||||
from aqt.operations.tag import find_and_replace_tag
|
||||
from aqt.qt import QDialog, Qt
|
||||
from aqt.utils import (
|
||||
HelpPage,
|
||||
@ -22,63 +24,10 @@ from aqt.utils import (
|
||||
save_combo_index_for_session,
|
||||
save_is_checked,
|
||||
saveGeom,
|
||||
tooltip,
|
||||
tr,
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
field_name: Optional[str],
|
||||
match_case: bool,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
field_name=field_name,
|
||||
match_case=match_case,
|
||||
),
|
||||
success=lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace_tag(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[int],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
match_case: bool,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
match_case=match_case,
|
||||
),
|
||||
success=lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FindAndReplaceDialog(QDialog):
|
||||
COMBO_NAME = "BrowserFindAndReplace"
|
||||
|
||||
@ -146,10 +95,9 @@ class FindAndReplaceDialog(QDialog):
|
||||
save_is_checked(self.form.re, self.COMBO_NAME + "Regex")
|
||||
save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase")
|
||||
|
||||
# tags?
|
||||
if self.form.field.currentIndex() == 1:
|
||||
# tags
|
||||
find_and_replace_tag(
|
||||
mw=self.mw,
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
@ -157,23 +105,22 @@ class FindAndReplaceDialog(QDialog):
|
||||
regex=regex,
|
||||
match_case=match_case,
|
||||
)
|
||||
return
|
||||
|
||||
if self.form.field.currentIndex() == 0:
|
||||
field = None
|
||||
else:
|
||||
field = self.field_names[self.form.field.currentIndex() - 2]
|
||||
# fields
|
||||
if self.form.field.currentIndex() == 0:
|
||||
field = None
|
||||
else:
|
||||
field = self.field_names[self.form.field.currentIndex() - 2]
|
||||
|
||||
find_and_replace(
|
||||
mw=self.mw,
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
replacement=replace,
|
||||
regex=regex,
|
||||
field_name=field,
|
||||
match_case=match_case,
|
||||
)
|
||||
find_and_replace(
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
replacement=replace,
|
||||
regex=regex,
|
||||
field_name=field,
|
||||
match_case=match_case,
|
||||
)
|
||||
|
||||
super().accept()
|
||||
|
||||
|
154
qt/aqt/main.py
154
qt/aqt/main.py
@ -21,7 +21,6 @@ from typing import (
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
TextIO,
|
||||
Tuple,
|
||||
@ -40,15 +39,7 @@ import aqt.toolbar
|
||||
import aqt.webview
|
||||
from anki import hooks
|
||||
from anki._backend import RustBackend as _RustBackend
|
||||
from anki.collection import (
|
||||
Collection,
|
||||
Config,
|
||||
OpChanges,
|
||||
OpChangesAfterUndo,
|
||||
OpChangesWithCount,
|
||||
OpChangesWithId,
|
||||
UndoStatus,
|
||||
)
|
||||
from anki.collection import Collection, Config, OpChanges, UndoStatus
|
||||
from anki.decks import DeckDict, DeckId
|
||||
from anki.hooks import runHook
|
||||
from anki.notes import NoteId
|
||||
@ -61,8 +52,8 @@ from aqt.emptycards import show_empty_cards
|
||||
from aqt.legacy import install_pylib_legacy
|
||||
from aqt.mediacheck import check_media_db
|
||||
from aqt.mediasync import MediaSyncer
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.operations.collection import undo
|
||||
from aqt.operations.deck import set_current_deck
|
||||
from aqt.profiles import ProfileManager as ProfileManagerType
|
||||
from aqt.qt import *
|
||||
from aqt.qt import sip
|
||||
@ -92,30 +83,6 @@ from aqt.utils import (
|
||||
tr,
|
||||
)
|
||||
|
||||
|
||||
class HasChangesProperty(Protocol):
|
||||
changes: OpChanges
|
||||
|
||||
|
||||
# either an OpChanges object, or an object with .changes on it. This bound
|
||||
# doesn't actually work for protobuf objects, so new protobuf objects will
|
||||
# either need to be added here, or cast at call time
|
||||
ResultWithChanges = TypeVar(
|
||||
"ResultWithChanges",
|
||||
bound=Union[
|
||||
OpChanges,
|
||||
OpChangesWithCount,
|
||||
OpChangesWithId,
|
||||
OpChangesAfterUndo,
|
||||
HasChangesProperty,
|
||||
],
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
PerformOpOptionalSuccessCallback = Optional[Callable[[ResultWithChanges], Any]]
|
||||
PerformOpOptionalFailureCallback = Optional[Callable[[Exception], Any]]
|
||||
|
||||
install_pylib_legacy()
|
||||
|
||||
MainWindowState = Literal[
|
||||
@ -123,6 +90,9 @@ MainWindowState = Literal[
|
||||
]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class AnkiQt(QMainWindow):
|
||||
col: Collection
|
||||
pm: ProfileManagerType
|
||||
@ -733,10 +703,9 @@ class AnkiQt(QMainWindow):
|
||||
) -> None:
|
||||
"""Run an operation that queries the DB on a background thread.
|
||||
|
||||
Similar interface to perform_op(), but intended to be used for operations
|
||||
that do not change collection state. Undo status will not be changed,
|
||||
and `operation_did_execute` will not fire. No progress window will
|
||||
be shown either.
|
||||
Intended to be used for operations that do not change collection
|
||||
state. Undo status will not be changed, and `operation_did_execute`
|
||||
will not fire. No progress window will be shown either.
|
||||
|
||||
`operations_will|did_execute` will still fire, so the UI can defer
|
||||
updates during a background task.
|
||||
@ -766,71 +735,6 @@ class AnkiQt(QMainWindow):
|
||||
# Resetting state
|
||||
##########################################################################
|
||||
|
||||
def perform_op(
|
||||
self,
|
||||
op: Callable[[], ResultWithChanges],
|
||||
*,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
failure: PerformOpOptionalFailureCallback = None,
|
||||
after_hooks: Optional[Callable[[], None]] = None,
|
||||
meta: OpMeta = OpMeta(),
|
||||
) -> None:
|
||||
"""Run the provided operation on a background thread.
|
||||
|
||||
op() should either return OpChanges, or an object with a 'changes'
|
||||
property. The changes will be passed to `operation_did_execute` so that
|
||||
the UI can decide whether it needs to update itself.
|
||||
|
||||
- Shows progress popup for the duration of the op.
|
||||
- Ensures the browser doesn't try to redraw during the operation, which can lead
|
||||
to a frozen UI
|
||||
- Updates undo state at the end of the operation
|
||||
- Commits changes
|
||||
- Fires the `operation_(will|did)_reset` hooks
|
||||
- Fires the legacy `state_did_reset` hook
|
||||
|
||||
Be careful not to call any UI routines in `op`, as that may crash Qt.
|
||||
This includes things select .selectedCards() in the browse screen.
|
||||
|
||||
success() will be called with the return value of op().
|
||||
|
||||
If op() throws an exception, it will be shown in a popup, or
|
||||
passed to failure() if it is provided.
|
||||
|
||||
after_hooks() will be called after hooks are fired, if it is provided.
|
||||
Components can use this to ignore change notices generated by operations
|
||||
they invoke themselves, or perform some subsequent action.
|
||||
"""
|
||||
|
||||
self._increase_background_ops()
|
||||
|
||||
def wrapped_done(future: Future) -> None:
|
||||
self._decrease_background_ops()
|
||||
# did something go wrong?
|
||||
if exception := future.exception():
|
||||
if isinstance(exception, Exception):
|
||||
if failure:
|
||||
failure(exception)
|
||||
else:
|
||||
showWarning(str(exception))
|
||||
return
|
||||
else:
|
||||
# BaseException like SystemExit; rethrow it
|
||||
future.result()
|
||||
|
||||
result = future.result()
|
||||
try:
|
||||
if success:
|
||||
success(result)
|
||||
finally:
|
||||
# update undo status
|
||||
status = self.col.undo_status()
|
||||
self._update_undo_actions_for_status_and_save(status)
|
||||
# fire change hooks
|
||||
self._fire_change_hooks_after_op_performed(result, after_hooks, meta)
|
||||
|
||||
self.taskman.with_progress(op, wrapped_done)
|
||||
|
||||
def _increase_background_ops(self) -> None:
|
||||
if not self._background_op_count:
|
||||
gui_hooks.backend_will_block()
|
||||
@ -842,27 +746,6 @@ class AnkiQt(QMainWindow):
|
||||
gui_hooks.backend_did_block()
|
||||
assert self._background_op_count >= 0
|
||||
|
||||
def _fire_change_hooks_after_op_performed(
|
||||
self,
|
||||
result: ResultWithChanges,
|
||||
after_hooks: Optional[Callable[[], None]],
|
||||
meta: OpMeta,
|
||||
) -> None:
|
||||
if isinstance(result, OpChanges):
|
||||
changes = result
|
||||
else:
|
||||
changes = result.changes
|
||||
|
||||
# fire new hook
|
||||
print("op changes:")
|
||||
print(changes)
|
||||
gui_hooks.operation_did_execute(changes, meta)
|
||||
# fire legacy hook so old code notices changes
|
||||
if self.col.op_made_changes(changes):
|
||||
gui_hooks.state_did_reset()
|
||||
if after_hooks:
|
||||
after_hooks()
|
||||
|
||||
def _synthesize_op_did_execute_from_reset(self) -> None:
|
||||
"""Fire the `operation_did_execute` hook with everything marked as changed,
|
||||
after legacy code has called .reset()"""
|
||||
@ -872,15 +755,17 @@ class AnkiQt(QMainWindow):
|
||||
setattr(op, field.name, True)
|
||||
gui_hooks.operation_did_execute(op, None)
|
||||
|
||||
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
|
||||
def on_operation_did_execute(
|
||||
self, changes: OpChanges, handler: Optional[object]
|
||||
) -> None:
|
||||
"Notify current screen of changes."
|
||||
focused = current_top_level_widget() == self
|
||||
if self.state == "review":
|
||||
dirty = self.reviewer.op_executed(changes, focused)
|
||||
dirty = self.reviewer.op_executed(changes, handler, focused)
|
||||
elif self.state == "overview":
|
||||
dirty = self.overview.op_executed(changes, focused)
|
||||
dirty = self.overview.op_executed(changes, handler, focused)
|
||||
elif self.state == "deckBrowser":
|
||||
dirty = self.deckBrowser.op_executed(changes, focused)
|
||||
dirty = self.deckBrowser.op_executed(changes, handler, focused)
|
||||
else:
|
||||
dirty = False
|
||||
|
||||
@ -908,7 +793,7 @@ class AnkiQt(QMainWindow):
|
||||
def reset(self, unused_arg: bool = False) -> None:
|
||||
"""Legacy method of telling UI to refresh after changes made to DB.
|
||||
|
||||
New code should use mw.perform_op() instead."""
|
||||
New code should use CollectionOp() instead."""
|
||||
if self.col:
|
||||
# fire new `operation_did_execute` hook first. If the overview
|
||||
# or review screen are currently open, they will rebuild the study
|
||||
@ -1218,7 +1103,7 @@ title="%s" %s>%s</button>""" % (
|
||||
|
||||
def undo(self) -> None:
|
||||
"Call collection_ops.py:undo() directly instead."
|
||||
undo(mw=self, parent=self)
|
||||
undo(parent=self)
|
||||
|
||||
def update_undo_actions(self, status: Optional[UndoStatus] = None) -> None:
|
||||
"""Update menu text and enable/disable menu item as appropriate.
|
||||
@ -1541,8 +1426,11 @@ title="%s" %s>%s</button>""" % (
|
||||
|
||||
ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"])
|
||||
if ret.name:
|
||||
self.col.decks.select(self.col.decks.id(ret.name))
|
||||
self.moveToState("overview")
|
||||
# fixme: this is silly, it should be returning an ID
|
||||
deck_id = self.col.decks.id(ret.name)
|
||||
set_current_deck(parent=self, deck_id=deck_id).success(
|
||||
lambda out: self.moveToState("overview")
|
||||
).run_in_background()
|
||||
|
||||
def onEmptyCards(self) -> None:
|
||||
show_empty_cards(self)
|
||||
|
@ -1,16 +1,142 @@
|
||||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures._base import Future
|
||||
from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union
|
||||
|
||||
import aqt
|
||||
from anki.collection import (
|
||||
Collection,
|
||||
OpChanges,
|
||||
OpChangesAfterUndo,
|
||||
OpChangesWithCount,
|
||||
OpChangesWithId,
|
||||
)
|
||||
from aqt.qt import QWidget
|
||||
from aqt.utils import showWarning
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpMeta:
|
||||
"""Metadata associated with an operation.
|
||||
class HasChangesProperty(Protocol):
|
||||
changes: OpChanges
|
||||
|
||||
The `handler` field can be used by screens to ignore change
|
||||
events they initiated themselves, if they have already made
|
||||
the required changes."""
|
||||
|
||||
handler: Optional[object] = None
|
||||
# either an OpChanges object, or an object with .changes on it. This bound
|
||||
# doesn't actually work for protobuf objects, so new protobuf objects will
|
||||
# either need to be added here, or cast at call time
|
||||
ResultWithChanges = TypeVar(
|
||||
"ResultWithChanges",
|
||||
bound=Union[
|
||||
OpChanges,
|
||||
OpChangesWithCount,
|
||||
OpChangesWithId,
|
||||
OpChangesAfterUndo,
|
||||
HasChangesProperty,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class CollectionOp(Generic[ResultWithChanges]):
|
||||
"""Helper to perform a mutating DB operation on a background thread, and update UI.
|
||||
|
||||
`op` should either return OpChanges, or an object with a 'changes'
|
||||
property. The changes will be passed to `operation_did_execute` so that
|
||||
the UI can decide whether it needs to update itself.
|
||||
|
||||
- Shows progress popup for the duration of the op.
|
||||
- Ensures the browser doesn't try to redraw during the operation, which can lead
|
||||
to a frozen UI
|
||||
- Updates undo state at the end of the operation
|
||||
- Commits changes
|
||||
- Fires the `operation_(will|did)_reset` hooks
|
||||
- Fires the legacy `state_did_reset` hook
|
||||
|
||||
Be careful not to call any UI routines in `op`, as that may crash Qt.
|
||||
This includes things select .selectedCards() in the browse screen.
|
||||
|
||||
`success` will be called with the return value of op().
|
||||
|
||||
If op() throws an exception, it will be shown in a popup, or
|
||||
passed to `failure` if it is provided.
|
||||
"""
|
||||
|
||||
_success: Optional[Callable[[ResultWithChanges], Any]] = None
|
||||
_failure: Optional[Optional[Callable[[Exception], Any]]] = None
|
||||
|
||||
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
||||
self._parent = parent
|
||||
self._op = op
|
||||
|
||||
def success(
|
||||
self, success: Optional[Callable[[ResultWithChanges], Any]]
|
||||
) -> CollectionOp[ResultWithChanges]:
|
||||
self._success = success
|
||||
return self
|
||||
|
||||
def failure(
|
||||
self, failure: Optional[Optional[Callable[[Exception], Any]]]
|
||||
) -> CollectionOp[ResultWithChanges]:
|
||||
self._failure = failure
|
||||
return self
|
||||
|
||||
def run_in_background(self, *, initiator: Optional[object] = None) -> None:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
|
||||
mw._increase_background_ops()
|
||||
|
||||
def wrapped_op() -> ResultWithChanges:
|
||||
assert mw
|
||||
return self._op(mw.col)
|
||||
|
||||
def wrapped_done(future: Future) -> None:
|
||||
assert mw
|
||||
mw._decrease_background_ops()
|
||||
# did something go wrong?
|
||||
if exception := future.exception():
|
||||
if isinstance(exception, Exception):
|
||||
if self._failure:
|
||||
self._failure(exception)
|
||||
else:
|
||||
showWarning(str(exception), self._parent)
|
||||
return
|
||||
else:
|
||||
# BaseException like SystemExit; rethrow it
|
||||
future.result()
|
||||
|
||||
result = future.result()
|
||||
try:
|
||||
if self._success:
|
||||
self._success(result)
|
||||
finally:
|
||||
# update undo status
|
||||
status = mw.col.undo_status()
|
||||
mw._update_undo_actions_for_status_and_save(status)
|
||||
# fire change hooks
|
||||
self._fire_change_hooks_after_op_performed(result, initiator)
|
||||
|
||||
mw.taskman.with_progress(wrapped_op, wrapped_done)
|
||||
|
||||
def _fire_change_hooks_after_op_performed(
|
||||
self,
|
||||
result: ResultWithChanges,
|
||||
handler: Optional[object],
|
||||
) -> None:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
|
||||
if isinstance(result, OpChanges):
|
||||
changes = result
|
||||
else:
|
||||
changes = result.changes
|
||||
|
||||
# fire new hook
|
||||
print("op changes:")
|
||||
print(changes)
|
||||
aqt.gui_hooks.operation_did_execute(changes, handler)
|
||||
# fire legacy hook so old code notices changes
|
||||
if mw.col.op_made_changes(changes):
|
||||
aqt.gui_hooks.state_did_reset()
|
||||
|
@ -6,13 +6,22 @@ from __future__ import annotations
|
||||
from typing import Sequence
|
||||
|
||||
from anki.cards import CardId
|
||||
from anki.collection import OpChanges
|
||||
from anki.decks import DeckId
|
||||
from aqt import AnkiQt
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.qt import QWidget
|
||||
|
||||
|
||||
def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardId], deck_id: DeckId) -> None:
|
||||
mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id))
|
||||
def set_card_deck(
|
||||
*, parent: QWidget, card_ids: Sequence[CardId], deck_id: DeckId
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.set_deck(card_ids, deck_id))
|
||||
|
||||
|
||||
def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[CardId], flag: int) -> None:
|
||||
mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids))
|
||||
def set_card_flag(
|
||||
*,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
flag: int,
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.set_user_flag_for_cards(flag, card_ids))
|
||||
|
@ -3,34 +3,37 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aqt
|
||||
from anki.collection import LegacyCheckpoint, LegacyReviewUndo, OpChangesAfterUndo
|
||||
from anki.collection import LegacyCheckpoint, LegacyReviewUndo
|
||||
from anki.errors import UndoEmpty
|
||||
from anki.types import assert_exhaustive
|
||||
from aqt import gui_hooks
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.qt import QWidget
|
||||
from aqt.utils import showInfo, showWarning, tooltip, tr
|
||||
|
||||
|
||||
def undo(*, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
||||
def undo(*, parent: QWidget) -> None:
|
||||
"Undo the last operation, and refresh the UI."
|
||||
|
||||
def on_success(out: OpChangesAfterUndo) -> None:
|
||||
mw.update_undo_actions(out.new_status)
|
||||
tooltip(tr.undo_action_undone(action=out.operation), parent=parent)
|
||||
|
||||
def on_failure(exc: Exception) -> None:
|
||||
if isinstance(exc, UndoEmpty):
|
||||
# backend has no undo, but there may be a checkpoint
|
||||
# or v1/v2 review waiting
|
||||
_legacy_undo(mw=mw, parent=parent)
|
||||
_legacy_undo(parent=parent)
|
||||
else:
|
||||
showWarning(str(exc), parent=parent)
|
||||
|
||||
mw.perform_op(mw.col.undo, success=on_success, failure=on_failure)
|
||||
CollectionOp(parent, lambda col: col.undo()).success(
|
||||
lambda out: tooltip(tr.undo_action_undone(action=out.operation), parent=parent)
|
||||
).failure(on_failure).run_in_background()
|
||||
|
||||
|
||||
def _legacy_undo(*, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
||||
def _legacy_undo(*, parent: QWidget) -> None:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
assert mw.col
|
||||
|
||||
reviewing = mw.state == "review"
|
||||
just_refresh_reviewer = False
|
||||
|
||||
|
@ -3,85 +3,80 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Optional, Sequence
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
||||
from anki.decks import DeckCollapseScope, DeckId
|
||||
from aqt import AnkiQt, QWidget
|
||||
from aqt.main import PerformOpOptionalSuccessCallback
|
||||
from aqt.operations import OpMeta
|
||||
from aqt import QWidget
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.utils import getOnlyText, tooltip, tr
|
||||
|
||||
|
||||
def remove_decks(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
deck_ids: Sequence[DeckId],
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.decks.remove(deck_ids),
|
||||
success=lambda out: tooltip(
|
||||
tr.browsing_cards_deleted(count=out.count), parent=parent
|
||||
),
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.decks.remove(deck_ids)).success(
|
||||
lambda out: tooltip(tr.browsing_cards_deleted(count=out.count), parent=parent)
|
||||
)
|
||||
|
||||
|
||||
def reparent_decks(
|
||||
*, mw: AnkiQt, parent: QWidget, deck_ids: Sequence[DeckId], new_parent: DeckId
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.decks.reparent(deck_ids=deck_ids, new_parent=new_parent),
|
||||
success=lambda out: tooltip(
|
||||
*, parent: QWidget, deck_ids: Sequence[DeckId], new_parent: DeckId
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.decks.reparent(deck_ids=deck_ids, new_parent=new_parent)
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.browsing_reparented_decks(count=out.count), parent=parent
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def rename_deck(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
deck_id: DeckId,
|
||||
new_name: str,
|
||||
after_rename: Callable[[], None] = None,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.decks.rename(deck_id, new_name), after_hooks=after_rename
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.decks.rename(deck_id, new_name),
|
||||
)
|
||||
|
||||
|
||||
def add_deck_dialog(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
default_text: str = "",
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
) -> Optional[CollectionOp[OpChangesWithId]]:
|
||||
if name := getOnlyText(
|
||||
tr.decks_new_deck_name(), default=default_text, parent=parent
|
||||
).strip():
|
||||
add_deck(mw=mw, name=name, success=success)
|
||||
return add_deck(parent=parent, name=name)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def add_deck(
|
||||
*, mw: AnkiQt, name: str, success: PerformOpOptionalSuccessCallback = None
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.decks.add_normal_deck_with_name(name),
|
||||
success=success,
|
||||
)
|
||||
def add_deck(*, parent: QWidget, name: str) -> CollectionOp[OpChangesWithId]:
|
||||
return CollectionOp(parent, lambda col: col.decks.add_normal_deck_with_name(name))
|
||||
|
||||
|
||||
def set_deck_collapsed(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
deck_id: DeckId,
|
||||
collapsed: bool,
|
||||
scope: DeckCollapseScope.V,
|
||||
handler: Optional[object] = None,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.decks.set_collapsed(
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.decks.set_collapsed(
|
||||
deck_id=deck_id, collapsed=collapsed, scope=scope
|
||||
),
|
||||
meta=OpMeta(handler=handler),
|
||||
)
|
||||
|
||||
|
||||
def set_current_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.decks.set_current(deck_id))
|
||||
|
@ -5,34 +5,60 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from anki.collection import OpChanges, OpChangesWithCount
|
||||
from anki.decks import DeckId
|
||||
from anki.notes import Note, NoteId
|
||||
from aqt import AnkiQt
|
||||
from aqt.main import PerformOpOptionalSuccessCallback
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.qt import QWidget
|
||||
from aqt.utils import tooltip, tr
|
||||
|
||||
|
||||
def add_note(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note: Note,
|
||||
target_deck_id: DeckId,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success)
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.add_note(note, target_deck_id))
|
||||
|
||||
|
||||
def update_note(*, mw: AnkiQt, note: Note, handler: Optional[object]) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.update_note(note),
|
||||
meta=OpMeta(handler=handler),
|
||||
)
|
||||
def update_note(*, parent: QWidget, note: Note) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.update_note(note))
|
||||
|
||||
|
||||
def remove_notes(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success)
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.remove_notes(note_ids)).success(
|
||||
lambda out: tooltip(tr.browsing_cards_deleted(count=out.count)),
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace(
|
||||
*,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
field_name: Optional[str],
|
||||
match_case: bool,
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
field_name=field_name,
|
||||
match_case=match_case,
|
||||
),
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
)
|
||||
)
|
||||
|
@ -7,28 +7,33 @@ from typing import Optional, Sequence
|
||||
|
||||
import aqt
|
||||
from anki.cards import CardId
|
||||
from anki.collection import CARD_TYPE_NEW, Config
|
||||
from anki.collection import (
|
||||
CARD_TYPE_NEW,
|
||||
Config,
|
||||
OpChanges,
|
||||
OpChangesWithCount,
|
||||
OpChangesWithId,
|
||||
)
|
||||
from anki.decks import DeckId
|
||||
from anki.notes import NoteId
|
||||
from anki.scheduler import FilteredDeckForUpdate
|
||||
from aqt import AnkiQt
|
||||
from aqt.main import PerformOpOptionalSuccessCallback
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.qt import *
|
||||
from aqt.utils import disable_help_button, getText, tooltip, tr
|
||||
|
||||
|
||||
def set_due_date_dialog(
|
||||
*,
|
||||
mw: aqt.AnkiQt,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
config_key: Optional[Config.String.Key.V],
|
||||
) -> None:
|
||||
) -> Optional[CollectionOp[OpChanges]]:
|
||||
assert aqt.mw
|
||||
if not card_ids:
|
||||
return
|
||||
return None
|
||||
|
||||
default_text = (
|
||||
mw.col.get_config_string(config_key) if config_key is not None else ""
|
||||
aqt.mw.col.get_config_string(config_key) if config_key is not None else ""
|
||||
)
|
||||
prompt = "\n".join(
|
||||
[
|
||||
@ -43,35 +48,38 @@ def set_due_date_dialog(
|
||||
title=tr.actions_set_due_date(),
|
||||
)
|
||||
if not success or not days.strip():
|
||||
return
|
||||
|
||||
mw.perform_op(
|
||||
lambda: mw.col.sched.set_due_date(card_ids, days, config_key),
|
||||
success=lambda _: tooltip(
|
||||
tr.scheduling_set_due_date_done(cards=len(card_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.sched.set_due_date(card_ids, days, config_key)
|
||||
).success(
|
||||
lambda _: tooltip(
|
||||
tr.scheduling_set_due_date_done(cards=len(card_ids)),
|
||||
parent=parent,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def forget_cards(
|
||||
*, mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> None:
|
||||
if not card_ids:
|
||||
return
|
||||
|
||||
mw.perform_op(
|
||||
lambda: mw.col.sched.schedule_cards_as_new(card_ids),
|
||||
success=lambda _: tooltip(
|
||||
*, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.sched.schedule_cards_as_new(card_ids)
|
||||
).success(
|
||||
lambda _: tooltip(
|
||||
tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def reposition_new_cards_dialog(
|
||||
*, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> None:
|
||||
*, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> Optional[CollectionOp[OpChangesWithCount]]:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
assert mw.col.db
|
||||
|
||||
row = mw.col.db.first(
|
||||
f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0"
|
||||
)
|
||||
@ -92,15 +100,14 @@ def reposition_new_cards_dialog(
|
||||
|
||||
frm.start.selectAll()
|
||||
if not d.exec_():
|
||||
return
|
||||
return None
|
||||
|
||||
start = frm.start.value()
|
||||
step = frm.step.value()
|
||||
randomize = frm.randomize.isChecked()
|
||||
shift = frm.shift.isChecked()
|
||||
|
||||
reposition_new_cards(
|
||||
mw=mw,
|
||||
return reposition_new_cards(
|
||||
parent=parent,
|
||||
card_ids=card_ids,
|
||||
starting_from=start,
|
||||
@ -112,89 +119,80 @@ def reposition_new_cards_dialog(
|
||||
|
||||
def reposition_new_cards(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
starting_from: int,
|
||||
step_size: int,
|
||||
randomize: bool,
|
||||
shift_existing: bool,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.sched.reposition_new_cards(
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.sched.reposition_new_cards(
|
||||
card_ids=card_ids,
|
||||
starting_from=starting_from,
|
||||
step_size=step_size,
|
||||
randomize=randomize,
|
||||
shift_existing=shift_existing,
|
||||
),
|
||||
success=lambda out: tooltip(
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.browsing_changed_new_position(count=out.count), parent=parent
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def suspend_cards(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success)
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.sched.suspend_cards(card_ids))
|
||||
|
||||
|
||||
def suspend_note(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
note_id: NoteId,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.taskman.run_in_background(
|
||||
lambda: mw.col.card_ids_of_note(note_id),
|
||||
lambda future: suspend_cards(mw=mw, card_ids=future.result(), success=success),
|
||||
)
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.sched.suspend_notes(note_ids))
|
||||
|
||||
|
||||
def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[CardId]) -> None:
|
||||
mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids))
|
||||
def unsuspend_cards(
|
||||
*, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.sched.unsuspend_cards(card_ids))
|
||||
|
||||
|
||||
def bury_cards(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success)
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.sched.bury_cards(card_ids))
|
||||
|
||||
|
||||
def bury_note(
|
||||
def bury_notes(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
note_id: NoteId,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.taskman.run_in_background(
|
||||
lambda: mw.col.card_ids_of_note(note_id),
|
||||
lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success),
|
||||
)
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.sched.bury_notes(note_ids))
|
||||
|
||||
|
||||
def rebuild_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None:
|
||||
mw.perform_op(lambda: mw.col.sched.rebuild_filtered_deck(deck_id))
|
||||
def rebuild_filtered_deck(
|
||||
*, parent: QWidget, deck_id: DeckId
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.sched.rebuild_filtered_deck(deck_id))
|
||||
|
||||
|
||||
def empty_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None:
|
||||
mw.perform_op(lambda: mw.col.sched.empty_filtered_deck(deck_id))
|
||||
def empty_filtered_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.sched.empty_filtered_deck(deck_id))
|
||||
|
||||
|
||||
def add_or_update_filtered_deck(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
deck: FilteredDeckForUpdate,
|
||||
success: PerformOpOptionalSuccessCallback,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.sched.add_or_update_filtered_deck(deck),
|
||||
success=success,
|
||||
)
|
||||
) -> CollectionOp[OpChangesWithId]:
|
||||
return CollectionOp(parent, lambda col: col.sched.add_or_update_filtered_deck(deck))
|
||||
|
@ -3,90 +3,116 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
from typing import Sequence
|
||||
|
||||
from anki.collection import OpChangesWithCount
|
||||
from anki.collection import OpChanges, OpChangesWithCount
|
||||
from anki.notes import NoteId
|
||||
from aqt import AnkiQt, QWidget
|
||||
from aqt.main import PerformOpOptionalSuccessCallback
|
||||
from aqt import QWidget
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.utils import showInfo, tooltip, tr
|
||||
|
||||
|
||||
def add_tags_to_notes(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
space_separated_tags: str,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), success=success
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.tags.bulk_add(note_ids, space_separated_tags)
|
||||
).success(
|
||||
lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent)
|
||||
)
|
||||
|
||||
|
||||
def remove_tags_from_notes(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
space_separated_tags: str,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), success=success
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.tags.bulk_remove(note_ids, space_separated_tags)
|
||||
).success(
|
||||
lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent)
|
||||
)
|
||||
|
||||
|
||||
def clear_unused_tags(*, mw: AnkiQt, parent: QWidget) -> None:
|
||||
mw.perform_op(
|
||||
mw.col.tags.clear_unused_tags,
|
||||
success=lambda out: tooltip(
|
||||
def clear_unused_tags(*, parent: QWidget) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(parent, lambda col: col.tags.clear_unused_tags()).success(
|
||||
lambda out: tooltip(
|
||||
tr.browsing_removed_unused_tags_count(count=out.count), parent=parent
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def rename_tag(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
current_name: str,
|
||||
new_name: str,
|
||||
after_rename: Callable[[], None],
|
||||
) -> None:
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
def success(out: OpChangesWithCount) -> None:
|
||||
if out.count:
|
||||
tooltip(tr.browsing_notes_updated(count=out.count), parent=parent)
|
||||
else:
|
||||
showInfo(tr.browsing_tag_rename_warning_empty(), parent=parent)
|
||||
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.rename(old=current_name, new=new_name),
|
||||
success=success,
|
||||
after_hooks=after_rename,
|
||||
)
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.tags.rename(old=current_name, new=new_name),
|
||||
).success(success)
|
||||
|
||||
|
||||
def remove_tags_from_all_notes(
|
||||
*, mw: AnkiQt, parent: QWidget, space_separated_tags: str
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.remove(space_separated_tags=space_separated_tags),
|
||||
success=lambda out: tooltip(
|
||||
tr.browsing_notes_updated(count=out.count), parent=parent
|
||||
),
|
||||
*, parent: QWidget, space_separated_tags: str
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.tags.remove(space_separated_tags=space_separated_tags)
|
||||
).success(
|
||||
lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent)
|
||||
)
|
||||
|
||||
|
||||
def reparent_tags(
|
||||
*, mw: AnkiQt, parent: QWidget, tags: Sequence[str], new_parent: str
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.reparent(tags=tags, new_parent=new_parent),
|
||||
success=lambda out: tooltip(
|
||||
tr.browsing_notes_updated(count=out.count), parent=parent
|
||||
),
|
||||
*, parent: QWidget, tags: Sequence[str], new_parent: str
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.tags.reparent(tags=tags, new_parent=new_parent)
|
||||
).success(
|
||||
lambda out: tooltip(tr.browsing_notes_updated(count=out.count), parent=parent)
|
||||
)
|
||||
|
||||
|
||||
def set_tag_collapsed(*, mw: AnkiQt, tag: str, collapsed: bool) -> None:
|
||||
mw.perform_op(lambda: mw.col.tags.set_collapsed(tag=tag, collapsed=collapsed))
|
||||
def set_tag_collapsed(
|
||||
*, parent: QWidget, tag: str, collapsed: bool
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed)
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace_tag(
|
||||
*,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[int],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
match_case: bool,
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.tags.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
match_case=match_case,
|
||||
),
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
@ -64,8 +64,10 @@ class Overview:
|
||||
if self._refresh_needed:
|
||||
self.refresh()
|
||||
|
||||
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
|
||||
if self.mw.col.op_affects_study_queue(changes):
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
) -> bool:
|
||||
if changes.study_queues:
|
||||
self._refresh_needed = True
|
||||
|
||||
if focused:
|
||||
@ -117,12 +119,14 @@ class Overview:
|
||||
return self.mw.col.decks.current()["dyn"]
|
||||
|
||||
def rebuild_current_filtered_deck(self) -> None:
|
||||
if self._current_deck_is_filtered():
|
||||
rebuild_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
|
||||
rebuild_filtered_deck(
|
||||
parent=self.mw, deck_id=self.mw.col.decks.selected()
|
||||
).run_in_background()
|
||||
|
||||
def empty_current_filtered_deck(self) -> None:
|
||||
if self._current_deck_is_filtered():
|
||||
empty_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
|
||||
empty_filtered_deck(
|
||||
parent=self.mw, deck_id=self.mw.col.decks.selected()
|
||||
).run_in_background()
|
||||
|
||||
def onCustomStudyKey(self) -> None:
|
||||
if not self._current_deck_is_filtered():
|
||||
|
@ -14,7 +14,7 @@ from PyQt5.QtCore import Qt
|
||||
|
||||
from anki import hooks
|
||||
from anki.cards import Card, CardId
|
||||
from anki.collection import Config, OpChanges
|
||||
from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||
from anki.tags import MARKED_TAG
|
||||
from anki.utils import stripHTML
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
@ -22,7 +22,7 @@ from aqt.operations.card import set_card_flag
|
||||
from aqt.operations.note import remove_notes
|
||||
from aqt.operations.scheduling import (
|
||||
bury_cards,
|
||||
bury_note,
|
||||
bury_notes,
|
||||
set_due_date_dialog,
|
||||
suspend_cards,
|
||||
suspend_note,
|
||||
@ -38,7 +38,6 @@ from aqt.webview import AnkiWebView
|
||||
|
||||
|
||||
class RefreshNeeded(Enum):
|
||||
NO = auto()
|
||||
NOTE_TEXT = auto()
|
||||
QUEUES = auto()
|
||||
|
||||
@ -71,7 +70,7 @@ class Reviewer:
|
||||
self._recordedAudio: Optional[str] = None
|
||||
self.typeCorrect: str = None # web init happens before this is set
|
||||
self.state: Optional[str] = None
|
||||
self._refresh_needed = RefreshNeeded.NO
|
||||
self._refresh_needed: Optional[RefreshNeeded] = None
|
||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
||||
@ -102,29 +101,25 @@ class Reviewer:
|
||||
self.mw.col.reset()
|
||||
self.nextCard()
|
||||
self.mw.fade_in_webview()
|
||||
self._refresh_needed = RefreshNeeded.NO
|
||||
self._refresh_needed = None
|
||||
elif self._refresh_needed is RefreshNeeded.NOTE_TEXT:
|
||||
self._redraw_current_card()
|
||||
self.mw.fade_in_webview()
|
||||
self._refresh_needed = RefreshNeeded.NO
|
||||
self._refresh_needed = None
|
||||
|
||||
def op_executed(self, changes: OpChanges, focused: bool) -> bool:
|
||||
if changes.note and changes.kind == OpChanges.UPDATE_NOTE_TAGS:
|
||||
self.card.load()
|
||||
self._update_mark_icon()
|
||||
elif changes.card and changes.kind == OpChanges.SET_CARD_FLAG:
|
||||
# fixme: v3 mtime check
|
||||
self.card.load()
|
||||
self._update_flag_icon()
|
||||
elif self.mw.col.op_affects_study_queue(changes):
|
||||
self._refresh_needed = RefreshNeeded.QUEUES
|
||||
elif changes.note or changes.notetype or changes.tag:
|
||||
self._refresh_needed = RefreshNeeded.NOTE_TEXT
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
) -> bool:
|
||||
if handler is not self:
|
||||
if changes.study_queues:
|
||||
self._refresh_needed = RefreshNeeded.QUEUES
|
||||
elif changes.editor:
|
||||
self._refresh_needed = RefreshNeeded.NOTE_TEXT
|
||||
|
||||
if focused and self._refresh_needed is not RefreshNeeded.NO:
|
||||
if focused and self._refresh_needed:
|
||||
self.refresh_if_needed()
|
||||
|
||||
return self._refresh_needed is not RefreshNeeded.NO
|
||||
return bool(self._refresh_needed)
|
||||
|
||||
def _redraw_current_card(self) -> None:
|
||||
self.card.load()
|
||||
@ -830,63 +825,70 @@ time = %(time)d;
|
||||
self.mw.onDeckConf(self.mw.col.decks.get(self.card.current_deck_id()))
|
||||
|
||||
def set_flag_on_current_card(self, desired_flag: int) -> None:
|
||||
def redraw_flag(out: OpChanges) -> None:
|
||||
self.card.load()
|
||||
self._update_flag_icon()
|
||||
|
||||
# need to toggle off?
|
||||
if self.card.user_flag() == desired_flag:
|
||||
flag = 0
|
||||
else:
|
||||
flag = desired_flag
|
||||
|
||||
set_card_flag(mw=self.mw, card_ids=[self.card.id], flag=flag)
|
||||
set_card_flag(parent=self.mw, card_ids=[self.card.id], flag=flag).success(
|
||||
redraw_flag
|
||||
).run_in_background(initiator=self)
|
||||
|
||||
def toggle_mark_on_current_note(self) -> None:
|
||||
def redraw_mark(out: OpChangesWithCount) -> None:
|
||||
self.card.load()
|
||||
self._update_mark_icon()
|
||||
|
||||
note = self.card.note()
|
||||
if note.has_tag(MARKED_TAG):
|
||||
remove_tags_from_notes(
|
||||
mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
|
||||
)
|
||||
parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
|
||||
).success(redraw_mark).run_in_background(initiator=self)
|
||||
else:
|
||||
add_tags_to_notes(
|
||||
mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
|
||||
)
|
||||
parent=self.mw,
|
||||
note_ids=[note.id],
|
||||
space_separated_tags=MARKED_TAG,
|
||||
).success(redraw_mark).run_in_background(initiator=self)
|
||||
|
||||
def on_set_due(self) -> None:
|
||||
if self.mw.state != "review" or not self.card:
|
||||
return
|
||||
|
||||
set_due_date_dialog(
|
||||
mw=self.mw,
|
||||
parent=self.mw,
|
||||
card_ids=[self.card.id],
|
||||
config_key=Config.String.SET_DUE_REVIEWER,
|
||||
)
|
||||
).run_in_background()
|
||||
|
||||
def suspend_current_note(self) -> None:
|
||||
suspend_note(
|
||||
mw=self.mw,
|
||||
note_id=self.card.nid,
|
||||
success=lambda _: tooltip(tr.studying_note_suspended()),
|
||||
)
|
||||
parent=self.mw,
|
||||
note_ids=[self.card.nid],
|
||||
).success(lambda _: tooltip(tr.studying_note_suspended())).run_in_background()
|
||||
|
||||
def suspend_current_card(self) -> None:
|
||||
suspend_cards(
|
||||
mw=self.mw,
|
||||
parent=self.mw,
|
||||
card_ids=[self.card.id],
|
||||
success=lambda _: tooltip(tr.studying_card_suspended()),
|
||||
)
|
||||
).success(lambda _: tooltip(tr.studying_card_suspended())).run_in_background()
|
||||
|
||||
def bury_current_note(self) -> None:
|
||||
bury_note(
|
||||
mw=self.mw,
|
||||
note_id=self.card.nid,
|
||||
success=lambda _: tooltip(tr.studying_note_buried()),
|
||||
)
|
||||
bury_notes(
|
||||
parent=self.mw,
|
||||
note_ids=[self.card.nid],
|
||||
).success(lambda _: tooltip(tr.studying_note_buried())).run_in_background()
|
||||
|
||||
def bury_current_card(self) -> None:
|
||||
bury_cards(
|
||||
mw=self.mw,
|
||||
parent=self.mw,
|
||||
card_ids=[self.card.id],
|
||||
success=lambda _: tooltip(tr.studying_card_buried()),
|
||||
)
|
||||
).success(lambda _: tooltip(tr.studying_card_buried())).run_in_background()
|
||||
|
||||
def delete_current_note(self) -> None:
|
||||
# need to check state because the shortcut is global to the main
|
||||
@ -894,14 +896,7 @@ time = %(time)d;
|
||||
if self.mw.state != "review" or not self.card:
|
||||
return
|
||||
|
||||
# fixme: pass this back from the backend method instead
|
||||
cnt = len(self.card.note().cards())
|
||||
|
||||
remove_notes(
|
||||
mw=self.mw,
|
||||
note_ids=[self.card.nid],
|
||||
success=lambda _: tooltip(tr.studying_note_and_its_card_deleted(count=cnt)),
|
||||
)
|
||||
remove_notes(parent=self.mw, note_ids=[self.card.nid]).run_in_background()
|
||||
|
||||
def onRecordVoice(self) -> None:
|
||||
def after_record(path: str) -> None:
|
||||
|
@ -16,7 +16,6 @@ from anki.types import assert_exhaustive
|
||||
from aqt import colors, gui_hooks
|
||||
from aqt.clayout import CardLayout
|
||||
from aqt.models import Models
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.operations.deck import (
|
||||
remove_decks,
|
||||
rename_deck,
|
||||
@ -173,6 +172,18 @@ class SidebarItem:
|
||||
)
|
||||
return self._search_matches_self or self._search_matches_child
|
||||
|
||||
def has_same_id(self, other: SidebarItem) -> bool:
|
||||
"True if `other` is same type, with same id/name."
|
||||
if other.item_type == self.item_type:
|
||||
if self.item_type == SidebarItemType.TAG:
|
||||
return self.full_name == other.full_name
|
||||
elif self.item_type == SidebarItemType.SAVED_SEARCH:
|
||||
return self.name == other.name
|
||||
else:
|
||||
return other.id == self.id
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class SidebarModel(QAbstractItemModel):
|
||||
def __init__(self, sidebar: SidebarTreeView, root: SidebarItem) -> None:
|
||||
@ -420,8 +431,10 @@ class SidebarTreeView(QTreeView):
|
||||
# Refreshing
|
||||
###########################
|
||||
|
||||
def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None:
|
||||
if changes.browser_sidebar and not meta.handler is self:
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
) -> None:
|
||||
if changes.browser_sidebar and not handler is self:
|
||||
self._refresh_needed = True
|
||||
if focused:
|
||||
self.refresh_if_needed()
|
||||
@ -431,13 +444,16 @@ class SidebarTreeView(QTreeView):
|
||||
self.refresh()
|
||||
self._refresh_needed = False
|
||||
|
||||
def refresh(
|
||||
self, is_current: Optional[Callable[[SidebarItem], bool]] = None
|
||||
) -> None:
|
||||
def refresh(self) -> None:
|
||||
"Refresh list. No-op if sidebar is not visible."
|
||||
if not self.isVisible():
|
||||
return
|
||||
|
||||
if self.model() and (idx := self.currentIndex()):
|
||||
current_item = self.model().item_for_index(idx)
|
||||
else:
|
||||
current_item = None
|
||||
|
||||
def on_done(root: SidebarItem) -> None:
|
||||
# user may have closed browser
|
||||
if sip.isdeleted(self):
|
||||
@ -453,8 +469,8 @@ class SidebarTreeView(QTreeView):
|
||||
self.search_for(self.current_search)
|
||||
else:
|
||||
self._expand_where_necessary(model)
|
||||
if is_current:
|
||||
self.restore_current(is_current)
|
||||
if current_item:
|
||||
self.restore_current(current_item)
|
||||
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
@ -463,8 +479,8 @@ class SidebarTreeView(QTreeView):
|
||||
|
||||
self.mw.query_op(self._root_tree, success=on_done)
|
||||
|
||||
def restore_current(self, is_current: Callable[[SidebarItem], bool]) -> None:
|
||||
if current := self.find_item(is_current):
|
||||
def restore_current(self, current: SidebarItem) -> None:
|
||||
if current := self.find_item(current.has_same_id):
|
||||
index = self.model().index_for_item(current)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
index, QItemSelectionModel.SelectCurrent
|
||||
@ -615,8 +631,8 @@ class SidebarTreeView(QTreeView):
|
||||
new_parent = DeckId(target.id)
|
||||
|
||||
reparent_decks(
|
||||
mw=self.mw, parent=self.browser, deck_ids=deck_ids, new_parent=new_parent
|
||||
)
|
||||
parent=self.browser, deck_ids=deck_ids, new_parent=new_parent
|
||||
).run_in_background()
|
||||
|
||||
return True
|
||||
|
||||
@ -636,7 +652,9 @@ class SidebarTreeView(QTreeView):
|
||||
else:
|
||||
new_parent = target.full_name
|
||||
|
||||
reparent_tags(mw=self.mw, parent=self.browser, tags=tags, new_parent=new_parent)
|
||||
reparent_tags(
|
||||
parent=self.browser, tags=tags, new_parent=new_parent
|
||||
).run_in_background()
|
||||
|
||||
return True
|
||||
|
||||
@ -931,8 +949,8 @@ class SidebarTreeView(QTreeView):
|
||||
def toggle_expand(node: TagTreeNode) -> Callable[[bool], None]:
|
||||
full_name = head + node.name
|
||||
return lambda expanded: set_tag_collapsed(
|
||||
mw=self.mw, tag=full_name, collapsed=not expanded
|
||||
)
|
||||
parent=self, tag=full_name, collapsed=not expanded
|
||||
).run_in_background()
|
||||
|
||||
for node in nodes:
|
||||
item = SidebarItem(
|
||||
@ -977,11 +995,12 @@ class SidebarTreeView(QTreeView):
|
||||
) -> None:
|
||||
def toggle_expand(node: DeckTreeNode) -> Callable[[bool], None]:
|
||||
return lambda expanded: set_deck_collapsed(
|
||||
mw=self.mw,
|
||||
parent=self,
|
||||
deck_id=DeckId(node.deck_id),
|
||||
collapsed=not expanded,
|
||||
scope=DeckCollapseScope.BROWSER,
|
||||
handler=self,
|
||||
).run_in_background(
|
||||
initiator=self,
|
||||
)
|
||||
|
||||
for node in nodes:
|
||||
@ -1164,27 +1183,27 @@ class SidebarTreeView(QTreeView):
|
||||
def rename_deck(self, item: SidebarItem, new_name: str) -> None:
|
||||
if not new_name:
|
||||
return
|
||||
new_name = item.name_prefix + new_name
|
||||
|
||||
# update UI immediately, to avoid redraw
|
||||
item.name = new_name
|
||||
|
||||
full_name = item.name_prefix + new_name
|
||||
deck_id = DeckId(item.id)
|
||||
|
||||
def after_fetch(deck: Deck) -> None:
|
||||
if new_name == deck.name:
|
||||
if full_name == deck.name:
|
||||
return
|
||||
|
||||
rename_deck(
|
||||
mw=self.mw,
|
||||
parent=self,
|
||||
deck_id=deck_id,
|
||||
new_name=new_name,
|
||||
after_rename=lambda: self.refresh(
|
||||
lambda other: other.item_type == SidebarItemType.DECK
|
||||
and other.id == item.id
|
||||
),
|
||||
)
|
||||
new_name=full_name,
|
||||
).run_in_background()
|
||||
|
||||
self.mw.query_op(lambda: self.mw.col.get_deck(deck_id), success=after_fetch)
|
||||
|
||||
def delete_decks(self, _item: SidebarItem) -> None:
|
||||
remove_decks(mw=self.mw, parent=self.browser, deck_ids=self._selected_decks())
|
||||
remove_decks(parent=self, deck_ids=self._selected_decks()).run_in_background()
|
||||
|
||||
# Tags
|
||||
###########################
|
||||
@ -1194,8 +1213,8 @@ class SidebarTreeView(QTreeView):
|
||||
item.name = "..."
|
||||
|
||||
remove_tags_from_all_notes(
|
||||
mw=self.mw, parent=self.browser, space_separated_tags=tags
|
||||
)
|
||||
parent=self.browser, space_separated_tags=tags
|
||||
).run_in_background()
|
||||
|
||||
def rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
||||
if not new_name or new_name == item.name:
|
||||
@ -1207,17 +1226,13 @@ class SidebarTreeView(QTreeView):
|
||||
new_name = item.name_prefix + new_name
|
||||
|
||||
item.name = new_name_base
|
||||
item.full_name = new_name
|
||||
|
||||
rename_tag(
|
||||
mw=self.mw,
|
||||
parent=self.browser,
|
||||
current_name=old_name,
|
||||
new_name=new_name,
|
||||
after_rename=lambda: self.refresh(
|
||||
lambda item: item.item_type == SidebarItemType.TAG
|
||||
and item.full_name == new_name
|
||||
),
|
||||
)
|
||||
).run_in_background()
|
||||
|
||||
# Saved searches
|
||||
####################################
|
||||
@ -1249,10 +1264,7 @@ class SidebarTreeView(QTreeView):
|
||||
return
|
||||
conf[name] = search
|
||||
self._set_saved_searches(conf)
|
||||
self.refresh(
|
||||
lambda item: item.item_type == SidebarItemType.SAVED_SEARCH
|
||||
and item.name == name
|
||||
)
|
||||
self.refresh()
|
||||
|
||||
def remove_saved_searches(self, _item: SidebarItem) -> None:
|
||||
selected = self._selected_saved_searches()
|
||||
@ -1276,10 +1288,8 @@ class SidebarTreeView(QTreeView):
|
||||
conf[new_name] = filt
|
||||
del conf[old_name]
|
||||
self._set_saved_searches(conf)
|
||||
self.refresh(
|
||||
lambda item: item.item_type == SidebarItemType.SAVED_SEARCH
|
||||
and item.name == new_name
|
||||
)
|
||||
item.name = new_name
|
||||
self.refresh()
|
||||
|
||||
def save_current_search(self) -> None:
|
||||
if (search := self._get_current_search()) is None:
|
||||
|
@ -175,4 +175,6 @@ class StudyDeck(QDialog):
|
||||
|
||||
QDialog.accept(self)
|
||||
|
||||
add_deck_dialog(mw=self.mw, parent=self, default_text=default, success=success)
|
||||
add_deck_dialog(parent=self, default_text=default).success(
|
||||
success
|
||||
).run_in_background()
|
||||
|
@ -29,7 +29,6 @@ from anki.errors import NotFoundError
|
||||
from anki.notes import Note, NoteId
|
||||
from anki.utils import ids2str, isWin
|
||||
from aqt import colors, gui_hooks
|
||||
from aqt.operations import OpMeta
|
||||
from aqt.qt import *
|
||||
from aqt.theme import theme_manager
|
||||
from aqt.utils import (
|
||||
@ -180,7 +179,9 @@ class Table:
|
||||
def redraw_cells(self) -> None:
|
||||
self._model.redraw_cells()
|
||||
|
||||
def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None:
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
) -> None:
|
||||
if changes.browser_table:
|
||||
self._model.mark_cache_stale()
|
||||
if focused:
|
||||
|
@ -4,7 +4,7 @@
|
||||
"""
|
||||
Helper for running tasks on background threads.
|
||||
|
||||
See mw.query_op() and mw.perform_op() for slightly higher-level routines.
|
||||
See mw.query_op() and CollectionOp() for higher-level routines.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
@ -41,7 +41,7 @@ from anki.sound import AVTag, TTSTag
|
||||
from anki.utils import checksum, isWin, tmpdir
|
||||
from aqt import gui_hooks
|
||||
from aqt.sound import OnDoneCallback, SimpleProcessPlayer
|
||||
from aqt.utils import tooltip
|
||||
from aqt.utils import tooltip, tr
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -569,10 +569,7 @@ if isWin:
|
||||
try:
|
||||
ret.result()
|
||||
except RuntimeError:
|
||||
# fixme: i18n if this turns out to happen frequently
|
||||
tooltip(
|
||||
"TTS failed to play. Please check available languages in system settings."
|
||||
)
|
||||
tooltip(tr.errors_windows_tts_runtime_error())
|
||||
return
|
||||
|
||||
# inject file into the top of the audio queue
|
||||
|
12
qt/mypy.ini
12
qt/mypy.ini
@ -9,17 +9,7 @@ check_untyped_defs = true
|
||||
disallow_untyped_defs = True
|
||||
strict_equality = true
|
||||
|
||||
[mypy-aqt.scheduling_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.note_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.card_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.deck_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.find_and_replace]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.tag_ops]
|
||||
[mypy-aqt.operations.*]
|
||||
no_strict_optional = false
|
||||
|
||||
[mypy-aqt.winpaths]
|
||||
|
@ -26,7 +26,6 @@ from anki.hooks import runFilter, runHook
|
||||
from anki.models import NotetypeDict
|
||||
from aqt.qt import QDialog, QEvent, QMenu, QWidget
|
||||
from aqt.tagedit import TagEdit
|
||||
import aqt.operations
|
||||
"""
|
||||
|
||||
# Hook list
|
||||
@ -464,14 +463,14 @@ hooks = [
|
||||
Hook(
|
||||
name="state_did_reset",
|
||||
legacy_hook="reset",
|
||||
doc="""Legacy 'reset' hook. Called by mw.reset() and mw.perform_op() to redraw the UI.
|
||||
doc="""Legacy 'reset' hook. Called by mw.reset() and CollectionOp() to redraw the UI.
|
||||
|
||||
New code should use `operation_did_execute` instead.
|
||||
""",
|
||||
),
|
||||
Hook(
|
||||
name="operation_did_execute",
|
||||
args=["changes: anki.collection.OpChanges", "meta: aqt.operations.OpMeta"],
|
||||
args=["changes: anki.collection.OpChanges", "handler: Optional[object]"],
|
||||
doc="""Called after an operation completes.
|
||||
Changes can be inspected to determine whether the UI needs updating.
|
||||
|
||||
@ -489,7 +488,7 @@ hooks = [
|
||||
),
|
||||
Hook(
|
||||
name="backend_will_block",
|
||||
doc="""Called before one or more operations are executed with mw.perform_op().
|
||||
doc="""Called before one or more DB tasks are run in the background.
|
||||
|
||||
Subscribers can use this to set a flag to avoid DB queries until the operation
|
||||
completes, as doing so will freeze the UI until the long-running operation
|
||||
@ -498,7 +497,7 @@ hooks = [
|
||||
),
|
||||
Hook(
|
||||
name="backend_did_block",
|
||||
doc="""Called after one or more operations are executed with mw.perform_op().
|
||||
doc="""Called after one or more DB tasks finish running in the background.
|
||||
Called regardless of the success of individual operations, and only called when
|
||||
there are no outstanding ops.
|
||||
""",
|
||||
|
42
repos.bzl
42
repos.bzl
@ -33,11 +33,11 @@ def register_repos():
|
||||
maybe(
|
||||
http_archive,
|
||||
name = "rules_rust",
|
||||
strip_prefix = "rules_rust-anki-2021-03-30",
|
||||
strip_prefix = "rules_rust-anki-2021-04-09",
|
||||
urls = [
|
||||
"https://github.com/ankitects/rules_rust/archive/anki-2021-03-30.tar.gz",
|
||||
"https://github.com/ankitects/rules_rust/archive/anki-2021-04-09.tar.gz",
|
||||
],
|
||||
sha256 = "ad6286615fd21f71db4490207aa8d5ecdf5f526643cd65d682458d92aa84ff85",
|
||||
sha256 = "2821b22e065c1b4dc73610b1d6ccbed7ed4d755b316e7e0641cd079b7abe4900",
|
||||
)
|
||||
|
||||
# python
|
||||
@ -87,40 +87,8 @@ def register_repos():
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
sha256 = "55a25a762fcf9c9b88ab54436581e671bc9f4f523cb5a1bd32459ebec7be68a8",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.2/rules_nodejs-3.2.2.tar.gz"],
|
||||
)
|
||||
|
||||
_ESBUILD_VERSION = "0.8.48" # reminder: update SHAs below when changing this value
|
||||
|
||||
http_archive(
|
||||
name = "esbuild_darwin",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-%s.tgz" % _ESBUILD_VERSION,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["bin/esbuild"])""",
|
||||
sha256 = "d21a722873ed24586f071973b77223553fca466946f3d7e3976eeaccb14424e6",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "esbuild_windows",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-%s.tgz" % _ESBUILD_VERSION,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["esbuild.exe"])""",
|
||||
sha256 = "fe5dcb97b4c47f9567012f0a45c19c655f3d2e0d76932f6dd12715dbebbd6eb0",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "esbuild_linux",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-%s.tgz" % _ESBUILD_VERSION,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["bin/esbuild"])""",
|
||||
sha256 = "60dabe141e5dfcf99e7113bded6012868132068a582a102b258fb7b1cfdac14b",
|
||||
sha256 = "f533eeefc8fe1ddfe93652ec50f82373d0c431f7faabd5e6323f6903195ef227",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.3.0/rules_nodejs-3.3.0.tar.gz"],
|
||||
)
|
||||
|
||||
# sass
|
||||
|
@ -120,7 +120,7 @@ service SchedulingService {
|
||||
rpc CongratsInfo(Empty) returns (CongratsInfoOut);
|
||||
rpc RestoreBuriedAndSuspendedCards(CardIds) returns (OpChanges);
|
||||
rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty);
|
||||
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges);
|
||||
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChangesWithCount);
|
||||
rpc EmptyFilteredDeck(DeckId) returns (OpChanges);
|
||||
rpc RebuildFilteredDeck(DeckId) returns (OpChangesWithCount);
|
||||
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges);
|
||||
@ -154,6 +154,8 @@ service DecksService {
|
||||
rpc GetOrCreateFilteredDeck(DeckId) returns (FilteredDeckForUpdate);
|
||||
rpc AddOrUpdateFilteredDeck(FilteredDeckForUpdate) returns (OpChangesWithId);
|
||||
rpc FilteredDeckOrderLabels(Empty) returns (StringList);
|
||||
rpc SetCurrentDeck(DeckId) returns (OpChanges);
|
||||
rpc GetCurrentDeck(Empty) returns (Deck);
|
||||
}
|
||||
|
||||
service NotesService {
|
||||
@ -163,7 +165,7 @@ service NotesService {
|
||||
rpc DefaultDeckForNotetype(NotetypeId) returns (DeckId);
|
||||
rpc UpdateNote(UpdateNoteIn) returns (OpChanges);
|
||||
rpc GetNote(NoteId) returns (Note);
|
||||
rpc RemoveNotes(RemoveNotesIn) returns (OpChanges);
|
||||
rpc RemoveNotes(RemoveNotesIn) returns (OpChangesWithCount);
|
||||
rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut);
|
||||
rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChanges);
|
||||
rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
|
||||
@ -1313,7 +1315,8 @@ message BuryOrSuspendCardsIn {
|
||||
BURY_USER = 2;
|
||||
}
|
||||
repeated int64 card_ids = 1;
|
||||
Mode mode = 2;
|
||||
repeated int64 note_ids = 2;
|
||||
Mode mode = 3;
|
||||
}
|
||||
|
||||
message ScheduleCardsAsNewIn {
|
||||
@ -1497,26 +1500,17 @@ message GetQueuedCardsOut {
|
||||
}
|
||||
|
||||
message OpChanges {
|
||||
// this is not an exhaustive list; we can add more cases as we need them
|
||||
enum Kind {
|
||||
OTHER = 0;
|
||||
UPDATE_NOTE_TAGS = 1;
|
||||
SET_CARD_FLAG = 2;
|
||||
UPDATE_NOTE = 3;
|
||||
}
|
||||
bool card = 1;
|
||||
bool note = 2;
|
||||
bool deck = 3;
|
||||
bool tag = 4;
|
||||
bool notetype = 5;
|
||||
bool config = 6;
|
||||
|
||||
Kind kind = 1;
|
||||
bool card = 2;
|
||||
bool note = 3;
|
||||
bool deck = 4;
|
||||
bool tag = 5;
|
||||
bool notetype = 6;
|
||||
bool preference = 7;
|
||||
|
||||
bool browser_table = 8;
|
||||
bool browser_sidebar = 9;
|
||||
bool editor = 10;
|
||||
bool study_queues = 11;
|
||||
bool browser_table = 7;
|
||||
bool browser_sidebar = 8;
|
||||
bool editor = 9;
|
||||
bool study_queues = 10;
|
||||
}
|
||||
|
||||
message UndoStatus {
|
||||
|
@ -163,7 +163,7 @@ impl From<String> for Variable {
|
||||
let kind = match name.as_str() {
|
||||
"cards" | "notes" | "count" | "amount" | "reviews" | "total" | "selected"
|
||||
| "kilobytes" | "daysStart" | "daysEnd" | "days" | "secs-per-card" | "remaining"
|
||||
| "hourStart" | "hourEnd" | "correct" | "decks" => VariableKind::Int,
|
||||
| "hourStart" | "hourEnd" | "correct" | "decks" | "changed" => VariableKind::Int,
|
||||
"average-seconds" | "cards-per-minute" => VariableKind::Float,
|
||||
"val" | "found" | "expected" | "part" | "percent" | "day" | "number" | "up"
|
||||
| "down" | "seconds" | "megs" => VariableKind::Any,
|
||||
|
@ -67,7 +67,7 @@ impl ConfigService for Backend {
|
||||
col.transact_no_undo(|col| {
|
||||
// ensure it's a well-formed object
|
||||
let val: Value = serde_json::from_slice(&input.value_json)?;
|
||||
col.set_config(input.key.as_str(), &val)
|
||||
col.set_config(input.key.as_str(), &val).map(|_| ())
|
||||
})
|
||||
})
|
||||
.map(Into::into)
|
||||
@ -98,7 +98,7 @@ impl ConfigService for Backend {
|
||||
self.with_col(|col| {
|
||||
col.transact_no_undo(|col| col.set_bool(input.key().into(), input.value))
|
||||
})
|
||||
.map(Into::into)
|
||||
.map(|_| ().into())
|
||||
}
|
||||
|
||||
fn get_config_string(&self, input: pb::config::String) -> Result<pb::String> {
|
||||
@ -113,7 +113,7 @@ impl ConfigService for Backend {
|
||||
self.with_col(|col| {
|
||||
col.transact_no_undo(|col| col.set_string(input.key().into(), &input.value))
|
||||
})
|
||||
.map(Into::into)
|
||||
.map(|_| ().into())
|
||||
}
|
||||
|
||||
fn get_preferences(&self, _input: pb::Empty) -> Result<pb::Preferences> {
|
||||
|
@ -187,6 +187,16 @@ impl DecksService for Backend {
|
||||
})
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn set_current_deck(&self, input: pb::DeckId) -> Result<pb::OpChanges> {
|
||||
self.with_col(|col| col.set_current_deck(input.did.into()))
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn get_current_deck(&self, _input: pb::Empty) -> Result<pb::Deck> {
|
||||
self.with_col(|col| col.get_current_deck())
|
||||
.map(|deck| (*deck).clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::DeckId> for DeckId {
|
||||
|
@ -63,7 +63,7 @@ impl NotesService for Backend {
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result<pb::OpChanges> {
|
||||
fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result<pb::OpChangesWithCount> {
|
||||
self.with_col(|col| {
|
||||
if !input.note_ids.is_empty() {
|
||||
col.remove_notes(
|
||||
@ -131,7 +131,7 @@ impl NotesService for Backend {
|
||||
fn cards_of_note(&self, input: pb::NoteId) -> Result<pb::CardIds> {
|
||||
self.with_col(|col| {
|
||||
col.storage
|
||||
.all_card_ids_of_note(NoteId(input.nid))
|
||||
.all_card_ids_of_note_in_order(NoteId(input.nid))
|
||||
.map(|v| pb::CardIds {
|
||||
cids: v.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use pb::op_changes::Kind;
|
||||
|
||||
use crate::{
|
||||
backend_proto as pb,
|
||||
ops::OpChanges,
|
||||
@ -10,27 +8,15 @@ use crate::{
|
||||
undo::{UndoOutput, UndoStatus},
|
||||
};
|
||||
|
||||
impl From<Op> for Kind {
|
||||
fn from(o: Op) -> Self {
|
||||
match o {
|
||||
Op::SetFlag => Kind::SetCardFlag,
|
||||
Op::UpdateTag => Kind::UpdateNoteTags,
|
||||
Op::UpdateNote => Kind::UpdateNote,
|
||||
_ => Kind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpChanges> for pb::OpChanges {
|
||||
fn from(c: OpChanges) -> Self {
|
||||
pb::OpChanges {
|
||||
kind: Kind::from(c.op) as i32,
|
||||
card: c.changes.card,
|
||||
note: c.changes.note,
|
||||
deck: c.changes.deck,
|
||||
tag: c.changes.tag,
|
||||
notetype: c.changes.notetype,
|
||||
preference: c.changes.preference,
|
||||
config: c.changes.config,
|
||||
browser_table: c.requires_browser_table_redraw(),
|
||||
browser_sidebar: c.requires_browser_sidebar_redraw(),
|
||||
editor: c.requires_editor_redraw(),
|
||||
|
@ -87,10 +87,18 @@ impl SchedulingService for Backend {
|
||||
})
|
||||
}
|
||||
|
||||
fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result<pb::OpChanges> {
|
||||
fn bury_or_suspend_cards(
|
||||
&self,
|
||||
input: pb::BuryOrSuspendCardsIn,
|
||||
) -> Result<pb::OpChangesWithCount> {
|
||||
self.with_col(|col| {
|
||||
let mode = input.mode();
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
|
||||
let cids = if input.card_ids.is_empty() {
|
||||
col.storage
|
||||
.card_ids_of_notes(&input.note_ids.into_newtype(NoteId))?
|
||||
} else {
|
||||
input.card_ids.into_newtype(CardId)
|
||||
};
|
||||
col.bury_or_suspend_cards(&cids, mode).map(Into::into)
|
||||
})
|
||||
}
|
||||
@ -105,7 +113,7 @@ impl SchedulingService for Backend {
|
||||
|
||||
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result<pb::OpChanges> {
|
||||
self.with_col(|col| {
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
|
||||
let cids = input.card_ids.into_newtype(CardId);
|
||||
let log = input.log;
|
||||
col.reschedule_cards_as_new(&cids, log).map(Into::into)
|
||||
})
|
||||
@ -114,12 +122,12 @@ impl SchedulingService for Backend {
|
||||
fn set_due_date(&self, input: pb::SetDueDateIn) -> Result<pb::OpChanges> {
|
||||
let config = input.config_key.map(Into::into);
|
||||
let days = input.days;
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
|
||||
let cids = input.card_ids.into_newtype(CardId);
|
||||
self.with_col(|col| col.set_due_date(&cids, &days, config).map(Into::into))
|
||||
}
|
||||
|
||||
fn sort_cards(&self, input: pb::SortCardsIn) -> Result<pb::OpChangesWithCount> {
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
|
||||
let cids = input.card_ids.into_newtype(CardId);
|
||||
let (start, step, random, shift) = (
|
||||
input.starting_from,
|
||||
input.step_size,
|
||||
|
@ -239,7 +239,7 @@ impl Collection {
|
||||
self.storage.set_search_table_to_card_ids(cards, false)?;
|
||||
let sched = self.scheduler_version();
|
||||
let usn = self.usn()?;
|
||||
self.transact(Op::SetDeck, |col| {
|
||||
self.transact(Op::SetCardDeck, |col| {
|
||||
for mut card in col.storage.all_searched_cards()? {
|
||||
if card.deck_id == deck_id {
|
||||
continue;
|
||||
|
@ -69,7 +69,7 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_bool(&mut self, key: BoolKey, value: bool) -> Result<()> {
|
||||
pub(crate) fn set_bool(&mut self, key: BoolKey, value: bool) -> Result<bool> {
|
||||
self.set_config(key, &value)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::ConfigKey;
|
||||
use crate::prelude::*;
|
||||
|
||||
use strum::IntoStaticStr;
|
||||
@ -20,11 +19,6 @@ impl DeckConfigKey {
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn get_current_deck_id(&self) -> DeckId {
|
||||
self.get_config_optional(ConfigKey::CurrentDeckId)
|
||||
.unwrap_or(DeckId(1))
|
||||
}
|
||||
|
||||
pub(crate) fn clear_aux_config_for_deck(&self, ntid: DeckId) -> Result<()> {
|
||||
self.remove_config_prefix(&build_aux_deck_key(ntid, ""))
|
||||
}
|
||||
@ -38,7 +32,7 @@ impl Collection {
|
||||
&mut self,
|
||||
did: DeckId,
|
||||
ntid: NotetypeId,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
let key = DeckConfigKey::LastNotetype.for_deck(did);
|
||||
self.set_config(key.as_str(), &ntid)
|
||||
}
|
||||
|
@ -94,7 +94,8 @@ impl Collection {
|
||||
self.get_config_optional(key).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<()>
|
||||
/// True if added, or new value is different.
|
||||
pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<bool>
|
||||
where
|
||||
K: Into<&'a str>,
|
||||
{
|
||||
@ -130,6 +131,7 @@ impl Collection {
|
||||
self.state.scheduler_info = None;
|
||||
if let Some(mins) = mins {
|
||||
self.set_config(ConfigKey::CreationOffset, &mins)
|
||||
.map(|_| ())
|
||||
} else {
|
||||
self.remove_config(ConfigKey::CreationOffset)
|
||||
}
|
||||
@ -141,7 +143,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_configured_utc_offset(&mut self, mins: i32) -> Result<()> {
|
||||
self.state.scheduler_info = None;
|
||||
self.set_config(ConfigKey::LocalOffset, &mins)
|
||||
self.set_config(ConfigKey::LocalOffset, &mins).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_v2_rollover(&self) -> Option<u8> {
|
||||
@ -151,7 +153,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_v2_rollover(&mut self, hour: u32) -> Result<()> {
|
||||
self.state.scheduler_info = None;
|
||||
self.set_config(ConfigKey::Rollover, &hour)
|
||||
self.set_config(ConfigKey::Rollover, &hour).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_next_card_position(&self) -> u32 {
|
||||
@ -168,6 +170,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_next_card_position(&mut self, pos: u32) -> Result<()> {
|
||||
self.set_config(ConfigKey::NextNewCardPosition, &pos)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn scheduler_version(&self) -> SchedulerVersion {
|
||||
@ -179,6 +182,7 @@ impl Collection {
|
||||
pub(crate) fn set_scheduler_version_config_key(&mut self, ver: SchedulerVersion) -> Result<()> {
|
||||
self.state.scheduler_info = None;
|
||||
self.set_config(ConfigKey::SchedulerVersion, &ver)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn learn_ahead_secs(&self) -> u32 {
|
||||
@ -188,6 +192,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_learn_ahead_secs(&mut self, secs: u32) -> Result<()> {
|
||||
self.set_config(ConfigKey::LearnAheadSecs, &secs)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_new_review_mix(&self) -> NewReviewMix {
|
||||
@ -200,6 +205,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_new_review_mix(&mut self, mix: NewReviewMix) -> Result<()> {
|
||||
self.set_config(ConfigKey::NewReviewMix, &(mix as u8))
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_first_day_of_week(&self) -> Weekday {
|
||||
@ -209,6 +215,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_first_day_of_week(&mut self, weekday: Weekday) -> Result<()> {
|
||||
self.set_config(ConfigKey::FirstDayOfWeek, &weekday)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_answer_time_limit_secs(&self) -> u32 {
|
||||
@ -218,6 +225,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_answer_time_limit_secs(&mut self, secs: u32) -> Result<()> {
|
||||
self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_unburied_day(&self) -> u32 {
|
||||
@ -227,6 +235,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_last_unburied_day(&mut self, day: u32) -> Result<()> {
|
||||
self.set_config(ConfigKey::LastUnburiedDay, &day)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_current_notetype_id(&mut self, ntid: NotetypeId) -> Result<()> {
|
||||
self.set_config(ConfigKey::CurrentNotetypeId, &ntid)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_aux_config_for_notetype(&self, ntid: NotetypeId) -> Result<()> {
|
||||
@ -43,7 +44,7 @@ impl Collection {
|
||||
|
||||
pub(crate) fn set_last_deck_for_notetype(&mut self, id: NotetypeId, did: DeckId) -> Result<()> {
|
||||
let key = NotetypeConfigKey::LastDeckAddedTo.for_notetype(id);
|
||||
self.set_config(key.as_str(), &did)
|
||||
self.set_config(key.as_str(), &did).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ impl Collection {
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn set_string(&mut self, key: StringKey, val: &str) -> Result<()> {
|
||||
pub(crate) fn set_string(&mut self, key: StringKey, val: &str) -> Result<bool> {
|
||||
self.set_config(key, &val)
|
||||
}
|
||||
}
|
||||
|
@ -21,16 +21,19 @@ impl Collection {
|
||||
.get_config_entry(&entry.key)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("config disappeared"))?;
|
||||
self.update_config_entry_undoable(entry, current)
|
||||
.map(|_| ())
|
||||
}
|
||||
UndoableConfigChange::Removed(entry) => self.add_config_entry_undoable(entry),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_config_undoable(&mut self, entry: Box<ConfigEntry>) -> Result<()> {
|
||||
/// True if added, or value changed.
|
||||
pub(super) fn set_config_undoable(&mut self, entry: Box<ConfigEntry>) -> Result<bool> {
|
||||
if let Some(original) = self.storage.get_config_entry(&entry.key)? {
|
||||
self.update_config_entry_undoable(entry, original)
|
||||
} else {
|
||||
self.add_config_entry_undoable(entry)
|
||||
self.add_config_entry_undoable(entry)?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,16 +52,19 @@ impl Collection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// True if new value differed.
|
||||
fn update_config_entry_undoable(
|
||||
&mut self,
|
||||
entry: Box<ConfigEntry>,
|
||||
original: Box<ConfigEntry>,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
if entry.value != original.value {
|
||||
self.save_undo(UndoableConfigChange::Updated(original));
|
||||
self.storage.set_config_entry(&entry)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
41
rslib/src/decks/current.rs
Normal file
41
rslib/src/decks/current.rs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{config::ConfigKey, prelude::*};
|
||||
|
||||
impl Collection {
|
||||
pub fn set_current_deck(&mut self, deck: DeckId) -> Result<OpOutput<()>> {
|
||||
self.transact(Op::SetCurrentDeck, |col| col.set_current_deck_inner(deck))
|
||||
}
|
||||
|
||||
/// Fetch the current deck, falling back to the default if the previously
|
||||
/// selected deck is invalid.
|
||||
pub fn get_current_deck(&mut self) -> Result<Arc<Deck>> {
|
||||
if let Some(deck) = self.get_deck(self.get_current_deck_id())? {
|
||||
return Ok(deck);
|
||||
}
|
||||
self.get_deck(DeckId(1))?.ok_or(AnkiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
/// The returned id may reference a deck that does not exist;
|
||||
/// prefer using get_current_deck() instead.
|
||||
pub(crate) fn get_current_deck_id(&self) -> DeckId {
|
||||
self.get_config_optional(ConfigKey::CurrentDeckId)
|
||||
.unwrap_or(DeckId(1))
|
||||
}
|
||||
|
||||
fn set_current_deck_inner(&mut self, deck: DeckId) -> Result<()> {
|
||||
if self.set_current_deck_id(deck)? {
|
||||
self.state.card_queues = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_current_deck_id(&mut self, did: DeckId) -> Result<bool> {
|
||||
self.set_config(ConfigKey::CurrentDeckId, &did)
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod counts;
|
||||
mod current;
|
||||
mod filtered;
|
||||
mod schema11;
|
||||
mod tree;
|
||||
|
@ -402,19 +402,21 @@ impl Collection {
|
||||
}
|
||||
|
||||
/// Remove provided notes, and any cards that use them.
|
||||
pub(crate) fn remove_notes(&mut self, nids: &[NoteId]) -> Result<OpOutput<()>> {
|
||||
pub(crate) fn remove_notes(&mut self, nids: &[NoteId]) -> Result<OpOutput<usize>> {
|
||||
let usn = self.usn()?;
|
||||
self.transact(Op::RemoveNote, |col| {
|
||||
let mut card_count = 0;
|
||||
for nid in nids {
|
||||
let nid = *nid;
|
||||
if let Some(_existing_note) = col.storage.get_note(nid)? {
|
||||
for card in col.storage.all_cards_of_note(nid)? {
|
||||
card_count += 1;
|
||||
col.remove_card_and_add_grave_undoable(card, usn)?;
|
||||
}
|
||||
col.remove_note_only_undoable(nid, usn)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(card_count)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub enum Op {
|
||||
RenameTag,
|
||||
ReparentTag,
|
||||
ScheduleAsNew,
|
||||
SetDeck,
|
||||
SetCardDeck,
|
||||
SetDueDate,
|
||||
SetFlag,
|
||||
SortCards,
|
||||
@ -34,6 +34,7 @@ pub enum Op {
|
||||
UpdateNote,
|
||||
UpdatePreferences,
|
||||
UpdateTag,
|
||||
SetCurrentDeck,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@ -55,7 +56,7 @@ impl Op {
|
||||
Op::UpdateNote => tr.undo_update_note(),
|
||||
Op::UpdatePreferences => tr.preferences_preferences(),
|
||||
Op::UpdateTag => tr.undo_update_tag(),
|
||||
Op::SetDeck => tr.browsing_change_deck(),
|
||||
Op::SetCardDeck => tr.browsing_change_deck(),
|
||||
Op::SetFlag => tr.undo_set_flag(),
|
||||
Op::FindAndReplace => tr.browsing_find_and_replace(),
|
||||
Op::ClearUnusedTags => tr.browsing_clear_unused_tags(),
|
||||
@ -68,6 +69,7 @@ impl Op {
|
||||
Op::RebuildFilteredDeck => tr.undo_build_filtered_deck(),
|
||||
Op::EmptyFilteredDeck => tr.studying_empty(),
|
||||
Op::ExpandCollapse => tr.undo_expand_collapse(),
|
||||
Op::SetCurrentDeck => tr.browsing_change_deck(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@ -80,7 +82,7 @@ pub struct StateChanges {
|
||||
pub deck: bool,
|
||||
pub tag: bool,
|
||||
pub notetype: bool,
|
||||
pub preference: bool,
|
||||
pub config: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -134,7 +136,12 @@ impl OpChanges {
|
||||
|
||||
pub fn requires_study_queue_rebuild(&self) -> bool {
|
||||
let c = &self.changes;
|
||||
!matches!(self.op, Op::AnswerCard | Op::ExpandCollapse)
|
||||
&& (c.card || c.deck || c.preference)
|
||||
if self.op == Op::AnswerCard {
|
||||
return false;
|
||||
}
|
||||
|
||||
c.card
|
||||
|| (c.deck && self.op != Op::ExpandCollapse)
|
||||
|| (c.config && matches!(self.op, Op::SetCurrentDeck))
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
pub(crate) use crate::types::IntoNewtypeVec;
|
||||
pub use crate::{
|
||||
card::{Card, CardId},
|
||||
collection::Collection,
|
||||
|
@ -33,7 +33,7 @@ mod test {
|
||||
let queued = col.next_card()?.unwrap();
|
||||
let nid = note.id;
|
||||
let cid = queued.card.id;
|
||||
let sibling_cid = col.storage.all_card_ids_of_note(nid)?[1];
|
||||
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
|
||||
|
||||
let assert_initial_state = |col: &mut Collection| -> Result<()> {
|
||||
let first = col.storage.get_card(cid)?.unwrap();
|
||||
|
@ -89,7 +89,8 @@ impl Collection {
|
||||
|
||||
/// Bury/suspend cards in search table, and clear it.
|
||||
/// Marks the cards as modified.
|
||||
fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<()> {
|
||||
fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<usize> {
|
||||
let mut count = 0;
|
||||
let usn = self.usn()?;
|
||||
let sched = self.scheduler_version();
|
||||
|
||||
@ -113,18 +114,21 @@ impl Collection {
|
||||
card.remove_from_learning();
|
||||
}
|
||||
card.queue = desired_queue;
|
||||
count += 1;
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.storage.clear_searched_cards_table()
|
||||
self.storage.clear_searched_cards_table()?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub fn bury_or_suspend_cards(
|
||||
&mut self,
|
||||
cids: &[CardId],
|
||||
mode: BuryOrSuspendMode,
|
||||
) -> Result<OpOutput<()>> {
|
||||
) -> Result<OpOutput<usize>> {
|
||||
let op = match mode {
|
||||
BuryOrSuspendMode::Suspend => Op::Suspend,
|
||||
BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => Op::Bury,
|
||||
@ -141,7 +145,7 @@ impl Collection {
|
||||
nid: NoteId,
|
||||
include_new: bool,
|
||||
include_reviews: bool,
|
||||
) -> Result<()> {
|
||||
) -> Result<usize> {
|
||||
self.storage
|
||||
.search_siblings_for_bury(cid, nid, include_new, include_reviews)?;
|
||||
self.bury_or_suspend_searched_cards(BuryOrSuspendMode::BurySched)
|
||||
|
@ -308,13 +308,26 @@ impl super::SqliteStorage {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn all_card_ids_of_note(&self, nid: NoteId) -> Result<Vec<CardId>> {
|
||||
pub(crate) fn all_card_ids_of_note_in_order(&self, nid: NoteId) -> Result<Vec<CardId>> {
|
||||
self.db
|
||||
.prepare_cached("select id from cards where nid = ? order by ord")?
|
||||
.query_and_then(&[nid], |r| Ok(CardId(r.get(0)?)))?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn card_ids_of_notes(&self, nids: &[NoteId]) -> Result<Vec<CardId>> {
|
||||
let mut stmt = self
|
||||
.db
|
||||
.prepare_cached("select id from cards where nid = ?")?;
|
||||
let mut cids = vec![];
|
||||
for nid in nids {
|
||||
for cid in stmt.query_map(&[nid], |row| row.get(0))? {
|
||||
cids.push(cid?);
|
||||
}
|
||||
}
|
||||
Ok(cids)
|
||||
}
|
||||
|
||||
/// Place matching card ids into the search table.
|
||||
pub(crate) fn search_siblings_for_bury(
|
||||
&self,
|
||||
|
@ -68,3 +68,18 @@ macro_rules! define_newtype {
|
||||
}
|
||||
|
||||
define_newtype!(Usn, i32);
|
||||
|
||||
pub(crate) trait IntoNewtypeVec {
|
||||
fn into_newtype<F, T>(self, func: F) -> Vec<T>
|
||||
where
|
||||
F: FnMut(i64) -> T;
|
||||
}
|
||||
|
||||
impl IntoNewtypeVec for Vec<i64> {
|
||||
fn into_newtype<F, T>(self, func: F) -> Vec<T>
|
||||
where
|
||||
F: FnMut(i64) -> T,
|
||||
{
|
||||
self.into_iter().map(func).collect()
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ impl UndoManager {
|
||||
UndoableChange::Tag(_) => changes.tag = true,
|
||||
UndoableChange::Revlog(_) => {}
|
||||
UndoableChange::Queue(_) => {}
|
||||
UndoableChange::Config(_) => {} // fixme: preferences?
|
||||
UndoableChange::Config(_) => changes.config = true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,8 @@ esbuild(
|
||||
args = [
|
||||
"--global-name=anki",
|
||||
"--inject:$(location //ts:protobuf-shim.js)",
|
||||
"--resolve-extensions=.mjs,.js",
|
||||
"--log-level=warning",
|
||||
],
|
||||
entry_point = "index.ts",
|
||||
external = [
|
||||
|
@ -36,6 +36,8 @@ esbuild(
|
||||
name = "editor",
|
||||
args = [
|
||||
"--loader:.svg=text",
|
||||
"--resolve-extensions=.mjs,.js",
|
||||
"--log-level=warning",
|
||||
],
|
||||
entry_point = "index_wrapper.ts",
|
||||
visibility = ["//visibility:public"],
|
||||
|
@ -3,11 +3,6 @@ load("//ts/esbuild:upstream.bzl", _esbuild = "esbuild_macro")
|
||||
def esbuild(name, **kwargs):
|
||||
_esbuild(
|
||||
name = name,
|
||||
tool = select({
|
||||
"@bazel_tools//src/conditions:darwin": "@esbuild_darwin//:bin/esbuild",
|
||||
"@bazel_tools//src/conditions:windows": "@esbuild_windows//:esbuild.exe",
|
||||
"@bazel_tools//src/conditions:linux_x86_64": "@esbuild_linux//:bin/esbuild",
|
||||
}),
|
||||
minify = select({
|
||||
"//:release": True,
|
||||
"//conditions:default": False,
|
||||
|
@ -0,0 +1,8 @@
|
||||
load(":toolchain.bzl", "define_default_toolchains", "esbuild_toolchain")
|
||||
|
||||
toolchain_type(
|
||||
name = "toolchain_type",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
define_default_toolchains()
|
4
ts/esbuild/README.md
Normal file
4
ts/esbuild/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
This folder vendors the esbuild support in rules_nodejs while we wait
|
||||
for some upstream changes to be applied:
|
||||
|
||||
- https://github.com/bazelbuild/rules_nodejs/pull/2545
|
45
ts/esbuild/esbuild_repo.bzl
Normal file
45
ts/esbuild/esbuild_repo.bzl
Normal file
@ -0,0 +1,45 @@
|
||||
""" Generated code; do not edit
|
||||
Update by running yarn update-esbuild-versions
|
||||
|
||||
Helper macro for fetching esbuild versions for internal tests and examples in rules_nodejs
|
||||
"""
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
load(":toolchain.bzl", "register_default_toolchains")
|
||||
|
||||
_VERSION = "0.11.5"
|
||||
|
||||
def esbuild_dependencies():
|
||||
"""Helper to install required dependencies for the esbuild rules"""
|
||||
|
||||
version = _VERSION
|
||||
|
||||
http_archive(
|
||||
name = "esbuild_darwin",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-%s.tgz" % version,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["bin/esbuild"])""",
|
||||
sha256 = "98436890727bdb0d4beddd9c9e07d0aeff0e8dfe0169f85e568eca0dd43f665e",
|
||||
)
|
||||
http_archive(
|
||||
name = "esbuild_windows",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-%s.tgz" % version,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["esbuild.exe"])""",
|
||||
sha256 = "589c8ff97210bd41de106e6317ce88f9e88d2cacfd8178ae1217f2b857ff6c3a",
|
||||
)
|
||||
http_archive(
|
||||
name = "esbuild_linux",
|
||||
urls = [
|
||||
"https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-%s.tgz" % version,
|
||||
],
|
||||
strip_prefix = "package",
|
||||
build_file_content = """exports_files(["bin/esbuild"])""",
|
||||
sha256 = "113c2e84895f4422a3676db4e15d9f01b2b4fac041edab25284fdb9574ba58a0",
|
||||
)
|
||||
|
||||
register_default_toolchains()
|
42
ts/esbuild/toolchain.bzl
Normal file
42
ts/esbuild/toolchain.bzl
Normal file
@ -0,0 +1,42 @@
|
||||
def _esbuild_toolchain_impl(ctx):
|
||||
return [platform_common.ToolchainInfo(
|
||||
binary = ctx.executable.binary,
|
||||
)]
|
||||
|
||||
esbuild_toolchain = rule(
|
||||
implementation = _esbuild_toolchain_impl,
|
||||
attrs = {
|
||||
"binary": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
|
||||
},
|
||||
)
|
||||
|
||||
_package_path = "@net_ankiweb_anki//ts/esbuild"
|
||||
|
||||
TOOLCHAIN = _package_path + ":toolchain_type"
|
||||
|
||||
_default_toolchains = [
|
||||
["@esbuild_darwin//:bin/esbuild", "macos"],
|
||||
["@esbuild_linux//:bin/esbuild", "linux"],
|
||||
["@esbuild_windows//:esbuild.exe", "windows"],
|
||||
]
|
||||
|
||||
def define_default_toolchains():
|
||||
for repo_path, platform in _default_toolchains:
|
||||
esbuild_toolchain(
|
||||
name = "esbuild_" + platform,
|
||||
binary = repo_path,
|
||||
)
|
||||
|
||||
native.toolchain(
|
||||
name = "esbuild_{}_toolchain".format(platform),
|
||||
exec_compatible_with = [
|
||||
"@platforms//os:" + platform,
|
||||
"@platforms//cpu:x86_64",
|
||||
],
|
||||
toolchain = ":esbuild_" + platform,
|
||||
toolchain_type = ":toolchain_type",
|
||||
)
|
||||
|
||||
def register_default_toolchains():
|
||||
for _, platform in _default_toolchains:
|
||||
native.register_toolchains(_package_path + ":esbuild_{}_toolchain".format(platform))
|
@ -1,17 +1,11 @@
|
||||
"""
|
||||
NOTE: this file was forked from the following repo (Apache2)
|
||||
https://github.com/bazelbuild/rules_nodejs/blob/c47b770a122e9614516df2e3fdca6fe0bf6e3420/packages/esbuild/esbuild.bzl
|
||||
|
||||
Local changes not in upstream:
|
||||
https://github.com/bazelbuild/rules_nodejs/pull/2545
|
||||
https://github.com/bazelbuild/rules_nodejs/pull/2564
|
||||
|
||||
esbuild rule
|
||||
"""
|
||||
|
||||
load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "JSModuleInfo", "NpmPackageInfo", "node_modules_aspect")
|
||||
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "MODULE_MAPPINGS_ASPECT_RESULTS_NAME", "module_mappings_aspect")
|
||||
load(":helpers.bzl", "filter_files", "generate_path_mapping", "resolve_js_input", "write_jsconfig_file")
|
||||
load(":toolchain.bzl", "TOOLCHAIN")
|
||||
|
||||
def _esbuild_impl(ctx):
|
||||
# For each dep, JSEcmaScriptModuleInfo is used if found, then JSModuleInfo and finally
|
||||
@ -35,6 +29,9 @@ def _esbuild_impl(ctx):
|
||||
elif hasattr(dep, "files"):
|
||||
deps_depsets.append(dep.files)
|
||||
|
||||
if DefaultInfo in dep:
|
||||
deps_depsets.append(dep[DefaultInfo].data_runfiles.files)
|
||||
|
||||
if NpmPackageInfo in dep:
|
||||
deps_depsets.append(dep[NpmPackageInfo].sources)
|
||||
npm_workspaces.append(dep[NpmPackageInfo].workspace)
|
||||
@ -61,7 +58,12 @@ def _esbuild_impl(ctx):
|
||||
args = ctx.actions.args()
|
||||
|
||||
args.add("--bundle", entry_point.path)
|
||||
args.add("--sourcemap")
|
||||
|
||||
if len(ctx.attr.sourcemap) > 0:
|
||||
args.add_joined(["--sourcemap", ctx.attr.sourcemap], join_with = "=")
|
||||
else:
|
||||
args.add("--sourcemap")
|
||||
|
||||
args.add("--preserve-symlinks")
|
||||
args.add_joined(["--platform", ctx.attr.platform], join_with = "=")
|
||||
args.add_joined(["--target", ctx.attr.target], join_with = "=")
|
||||
@ -70,8 +72,8 @@ def _esbuild_impl(ctx):
|
||||
args.add_all(ctx.attr.define, format_each = "--define:%s")
|
||||
args.add_all(ctx.attr.external, format_each = "--external:%s")
|
||||
|
||||
# disable the error limit and show all errors
|
||||
args.add_joined(["--error-limit", "0"], join_with = "=")
|
||||
# disable the log limit and show all logs
|
||||
args.add_joined(["--log-limit", "0"], join_with = "=")
|
||||
|
||||
if ctx.attr.minify:
|
||||
args.add("--minify")
|
||||
@ -94,8 +96,14 @@ def _esbuild_impl(ctx):
|
||||
args.add_joined(["--outdir", js_out.path], join_with = "=")
|
||||
else:
|
||||
js_out = ctx.outputs.output
|
||||
outputs.append(js_out)
|
||||
|
||||
js_out_map = ctx.outputs.output_map
|
||||
outputs.extend([js_out, js_out_map])
|
||||
if ctx.attr.sourcemap != "inline":
|
||||
if js_out_map == None:
|
||||
fail("output_map must be specified if sourcemap is not set to 'inline'")
|
||||
outputs.append(js_out_map)
|
||||
|
||||
if ctx.outputs.output_css:
|
||||
outputs.append(ctx.outputs.output_css)
|
||||
|
||||
@ -110,15 +118,23 @@ def _esbuild_impl(ctx):
|
||||
|
||||
args.add_all([ctx.expand_location(arg) for arg in ctx.attr.args])
|
||||
|
||||
env = {}
|
||||
if ctx.attr.max_threads > 0:
|
||||
env["GOMAXPROCS"] = str(ctx.attr.max_threads)
|
||||
|
||||
execution_requirements = {}
|
||||
if "no-remote-exec" in ctx.attr.tags:
|
||||
execution_requirements = {"no-remote-exec": "1"}
|
||||
|
||||
ctx.actions.run(
|
||||
inputs = inputs,
|
||||
outputs = outputs,
|
||||
executable = ctx.executable.tool,
|
||||
executable = ctx.toolchains[TOOLCHAIN].binary,
|
||||
arguments = [args],
|
||||
progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.output_dir else "Splitting", entry_point.short_path),
|
||||
execution_requirements = {
|
||||
"no-remote-exec": "1",
|
||||
},
|
||||
execution_requirements = execution_requirements,
|
||||
mnemonic = "esbuild",
|
||||
env = env,
|
||||
)
|
||||
|
||||
return [
|
||||
@ -144,6 +160,7 @@ esbuild(
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
See https://esbuild.github.io/api/#define for more details
|
||||
""",
|
||||
),
|
||||
@ -160,6 +177,7 @@ See https://esbuild.github.io/api/#define for more details
|
||||
"external": attr.string_list(
|
||||
default = [],
|
||||
doc = """A list of module names that are treated as external and not included in the resulting bundle
|
||||
|
||||
See https://esbuild.github.io/api/#external for more details
|
||||
""",
|
||||
),
|
||||
@ -168,6 +186,7 @@ See https://esbuild.github.io/api/#external for more details
|
||||
mandatory = False,
|
||||
doc = """The output format of the bundle, defaults to iife when platform is browser
|
||||
and cjs when platform is node. If performing code splitting, defaults to esm.
|
||||
|
||||
See https://esbuild.github.io/api/#format for more details
|
||||
""",
|
||||
),
|
||||
@ -175,11 +194,20 @@ See https://esbuild.github.io/api/#format for more details
|
||||
doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'.
|
||||
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""",
|
||||
),
|
||||
"max_threads": attr.int(
|
||||
mandatory = False,
|
||||
doc = """Sets the `GOMAXPROCS` variable to limit the number of threads that esbuild can run with.
|
||||
This can be useful if running many esbuild rule invocations in parallel, which has the potential to cause slowdown.
|
||||
For general use, leave this attribute unset.
|
||||
""",
|
||||
),
|
||||
"minify": attr.bool(
|
||||
default = False,
|
||||
doc = """Minifies the bundle with the built in minification.
|
||||
Removes whitespace, shortens identifieres and uses equivalent but shorter syntax.
|
||||
|
||||
Sets all --minify-* flags
|
||||
|
||||
See https://esbuild.github.io/api/#minify for more details
|
||||
""",
|
||||
),
|
||||
@ -190,6 +218,7 @@ See https://esbuild.github.io/api/#minify for more details
|
||||
"output_dir": attr.bool(
|
||||
default = False,
|
||||
doc = """If true, esbuild produces an output directory containing all the output files from code splitting
|
||||
|
||||
See https://esbuild.github.io/api/#splitting for more details
|
||||
""",
|
||||
),
|
||||
@ -205,13 +234,23 @@ See https://esbuild.github.io/api/#splitting for more details
|
||||
default = "browser",
|
||||
values = ["node", "browser", "neutral", ""],
|
||||
doc = """The platform to bundle for.
|
||||
|
||||
See https://esbuild.github.io/api/#platform for more details
|
||||
""",
|
||||
),
|
||||
"sourcemap": attr.string(
|
||||
values = ["external", "inline", "both", ""],
|
||||
mandatory = False,
|
||||
doc = """Defines where sourcemaps are output and how they are included in the bundle. By default, a separate `.js.map` file is generated and referenced by the bundle. If 'external', a separate `.js.map` file is generated but not referenced by the bundle. If 'inline', a sourcemap is generated and its contents are inlined into the bundle (and no external sourcemap file is created). If 'both', a sourcemap is inlined and a `.js.map` file is created.
|
||||
|
||||
See https://esbuild.github.io/api/#sourcemap for more details
|
||||
""",
|
||||
),
|
||||
"sources_content": attr.bool(
|
||||
mandatory = False,
|
||||
default = False,
|
||||
doc = """If False, omits the `sourcesContent` field from generated source maps
|
||||
|
||||
See https://esbuild.github.io/api/#sources-content for more details
|
||||
""",
|
||||
),
|
||||
@ -219,36 +258,37 @@ See https://esbuild.github.io/api/#sources-content for more details
|
||||
allow_files = True,
|
||||
default = [],
|
||||
doc = """Non-entry point JavaScript source files from the workspace.
|
||||
|
||||
You must not repeat file(s) passed to entry_point""",
|
||||
),
|
||||
"target": attr.string(
|
||||
default = "es2015",
|
||||
doc = """Environment target (e.g. es2017, chrome58, firefox57, safari11,
|
||||
edge16, node10, default esnext)
|
||||
|
||||
See https://esbuild.github.io/api/#target for more details
|
||||
""",
|
||||
),
|
||||
"tool": attr.label(
|
||||
allow_single_file = True,
|
||||
mandatory = True,
|
||||
executable = True,
|
||||
cfg = "exec",
|
||||
doc = "An executable for the esbuild binary",
|
||||
),
|
||||
},
|
||||
implementation = _esbuild_impl,
|
||||
doc = """Runs the esbuild bundler under Bazel
|
||||
|
||||
For further information about esbuild, see https://esbuild.github.io/
|
||||
""",
|
||||
toolchains = [
|
||||
TOOLCHAIN,
|
||||
],
|
||||
)
|
||||
|
||||
def esbuild_macro(name, output_dir = False, output_css = False, **kwargs):
|
||||
"""esbuild helper macro around the `esbuild_bundle` rule
|
||||
|
||||
For a full list of attributes, see the `esbuild_bundle` rule
|
||||
|
||||
Args:
|
||||
name: The name used for this rule and output files
|
||||
output_dir: If `True`, produce a code split bundle in an output directory
|
||||
output_css: If `True`, declare a .css file will be outputted, which is the
|
||||
output_css: If `True`, declare name.css as an output, which is the
|
||||
case when your code imports a css file.
|
||||
**kwargs: All other args from `esbuild_bundle`
|
||||
"""
|
||||
@ -260,10 +300,19 @@ def esbuild_macro(name, output_dir = False, output_css = False, **kwargs):
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
output = "%s.js" % name
|
||||
if "output" in kwargs:
|
||||
output = kwargs.pop("output")
|
||||
|
||||
output_map = None
|
||||
sourcemap = kwargs.get("sourcemap", None)
|
||||
if sourcemap != "inline":
|
||||
output_map = "%s.map" % output
|
||||
|
||||
esbuild(
|
||||
name = name,
|
||||
output = "%s.js" % name,
|
||||
output_map = "%s.js.map" % name,
|
||||
output = output,
|
||||
output_map = output_map,
|
||||
output_css = None if not output_css else "%s.css" % name,
|
||||
**kwargs
|
||||
)
|
||||
|
@ -53,6 +53,8 @@ esbuild(
|
||||
args = [
|
||||
"--global-name=anki",
|
||||
"--inject:$(location //ts:protobuf-shim.js)",
|
||||
"--resolve-extensions=.mjs,.js",
|
||||
"--log-level=warning",
|
||||
],
|
||||
entry_point = "index.ts",
|
||||
external = [
|
||||
|
Loading…
Reference in New Issue
Block a user