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