diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 01845a74b..d2375c041 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -533,26 +533,8 @@ select id from notes where id in %s and id not in (select nid from cards)""" self._remNotes(nids) def emptyCids(self) -> List[int]: - """Returns IDs of empty cards.""" - rem: List[int] = [] - for m in self.models.all(): - rem += self.genCards(self.models.nids(m)) - return rem - - def emptyCardReport(self, cids) -> str: - rep = "" - for ords, cnt, flds in self.db.all( - """ -select group_concat(ord+1), count(), flds from cards c, notes n -where c.nid = n.id and c.id in %s group by nid""" - % ids2str(cids) - ): - rep += self.tr( - TR.EMPTY_CARDS_CARD_LINE, - **{"card-numbers": ords, "fields": flds.replace("\x1f", " / ")}, - ) - rep += "\n\n" - return rep + print("emptyCids() will go away") + return [] # Field checksums and sorting fields ########################################################################## diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 8acca1811..6b52b3fc3 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -700,6 +700,12 @@ class RustBackend: except NotFoundError: return None + def empty_cards_report(self) -> pb.EmptyCardsReport: + return self._run_command( + pb.BackendInput(get_empty_cards=pb.Empty()), release_gil=True + ).get_empty_cards + + def translate_string_in( key: TR, **kwargs: Union[str, int, float] ) -> pb.TranslateStringIn: diff --git a/pylib/tests/test_cards.py b/pylib/tests/test_cards.py index af1ddae58..82953b8dc 100644 --- a/pylib/tests/test_cards.py +++ b/pylib/tests/test_cards.py @@ -72,7 +72,9 @@ def test_genrem(): t = m["tmpls"][1] t["qfmt"] = "{{Back}}" mm.save(m, templates=True) - d.remCards(d.emptyCids()) + rep = d.backend.empty_cards_report() + for note in rep.notes: + d.remCards(note.card_ids) assert len(f.cards()) == 1 # if we add to the note, a card should be automatically generated f.load() diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py new file mode 100644 index 000000000..f3174130e --- /dev/null +++ b/qt/aqt/emptycards.py @@ -0,0 +1,96 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import re + +import aqt +from anki.backend_pb2 import EmptyCardsReport, NoteWithEmptyCards +from aqt.qt import QDialog, QDialogButtonBox, qconnect +from aqt.utils import TR, restoreGeom, saveGeom, tooltip, tr + + +def show_empty_cards(mw: aqt.main.AnkiQt) -> None: + mw.progress.start() + + def on_done(fut): + mw.progress.finish() + report: EmptyCardsReport = fut.result() + if not report.notes: + tooltip(tr(TR.EMPTY_CARDS_NOT_FOUND)) + return + diag = EmptyCardsDialog(mw, report) + diag.show() + + mw.taskman.run_in_background(mw.col.backend.empty_cards_report, on_done) + + +class EmptyCardsDialog(QDialog): + silentlyClose = True + + def __init__(self, mw: aqt.main.AnkiQt, report: EmptyCardsReport) -> None: + super().__init__(mw) + self.mw = mw.weakref() + self.report = report + self.form = aqt.forms.emptycards.Ui_Dialog() + self.form.setupUi(self) + restoreGeom(self, "emptycards") + self.setWindowTitle(tr(TR.EMPTY_CARDS_WINDOW_TITLE)) + self.form.keep_notes.setText(tr(TR.EMPTY_CARDS_PRESERVE_NOTES_CHECKBOX)) + self.form.webview.title = "empty cards" + self.form.webview.set_bridge_command(self._on_note_link_clicked, self) + + # make the note ids clickable + html = re.sub( + r"\[anki:nid:(\d+)\]", + "\\1: ", + report.report, + ) + style = "" + self.form.webview.stdHtml(style + html, context=self) + + def on_finished(code): + saveGeom(self, "emptycards") + + qconnect(self.finished, on_finished) + + self._delete_button = self.form.buttonBox.addButton( + tr(TR.EMPTY_CARDS_DELETE_BUTTON), QDialogButtonBox.ActionRole + ) + self._delete_button.setAutoDefault(False) + self._delete_button.clicked.connect(self._on_delete) + + def _on_note_link_clicked(self, link): + browser = aqt.dialogs.open("Browser", self.mw) + browser.form.searchEdit.lineEdit().setText(link) + browser.onSearchActivated() + + def _on_delete(self): + self.mw.progress.start() + + def delete(): + return self._delete_cards(self.form.keep_notes.isChecked()) + + def on_done(fut): + self.mw.progress.finish() + try: + count = fut.result() + finally: + self.close() + tooltip(tr(TR.EMPTY_CARDS_DELETED_COUNT, cards=count)) + + self.mw.taskman.run_in_background(delete, on_done) + + def _delete_cards(self, keep_notes): + to_delete = [] + for note in self.report.notes: + note: NoteWithEmptyCards = note + if keep_notes and note.will_delete_note: + # leave first card + to_delete.extend(note.card_ids[1:]) + else: + to_delete.extend(note.card_ids) + + self.mw.col.remCards(to_delete) + return len(to_delete) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 0743831c7..c8fbd594d 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -10,6 +10,7 @@ import os import re import signal import time +import weakref import zipfile from argparse import Namespace from concurrent.futures import Future @@ -35,6 +36,7 @@ from anki.storage import Collection from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from aqt import gui_hooks from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user +from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer @@ -53,7 +55,6 @@ from aqt.utils import ( openLink, restoreGeom, restoreState, - saveGeom, showInfo, showText, showWarning, @@ -161,6 +162,10 @@ class AnkiQt(QMainWindow): self.setupProfile() + def weakref(self) -> AnkiQt: + "Shortcut to create a weak reference that doesn't break code completion." + return weakref.proxy(self) # type: ignore + # Profiles ########################################################################## @@ -1336,34 +1341,8 @@ will be lost. Continue?""" self.col.decks.select(self.col.decks.id(ret.name)) self.moveToState("overview") - def onEmptyCards(self): - self.progress.start(immediate=True) - cids = self.col.emptyCids() - cids = gui_hooks.empty_cards_will_be_deleted(cids) - if not cids: - self.progress.finish() - tooltip(_("No empty cards.")) - return - report = self.col.emptyCardReport(cids) - self.progress.finish() - part1 = ngettext("%d card", "%d cards", len(cids)) % len(cids) - part1 = _("%s to delete:") % part1 - diag, box = showText(part1 + "\n\n" + report, run=False, geomKey="emptyCards") - box.addButton(_("Delete Cards"), QDialogButtonBox.AcceptRole) - box.button(QDialogButtonBox.Close).setDefault(True) - - def onDelete(): - saveGeom(diag, "emptyCards") - QDialog.accept(diag) - self.checkpoint(_("Delete Empty")) - self.col.remCards(cids) - tooltip( - ngettext("%d card deleted.", "%d cards deleted.", len(cids)) % len(cids) - ) - self.reset() - - qconnect(box.accepted, onDelete) - diag.show() + def onEmptyCards(self) -> None: + show_empty_cards(self) # Debugging ###################################################################### diff --git a/qt/designer/emptycards.ui b/qt/designer/emptycards.ui new file mode 100644 index 000000000..aee7f7602 --- /dev/null +++ b/qt/designer/emptycards.ui @@ -0,0 +1,128 @@ + + + Dialog + + + + 0 + 0 + 531 + 345 + + + + EMPTY_CARDS + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + about:blank + + + + + + + + 12 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + KEEP_NOTES + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + AnkiWebView + QWidget +
aqt/webview
+ 1 +
+
+ + buttonBox + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/rslib/ftl/empty-cards.ftl b/rslib/ftl/empty-cards.ftl index f4227e72d..7cbbaa5b7 100644 --- a/rslib/ftl/empty-cards.ftl +++ b/rslib/ftl/empty-cards.ftl @@ -4,3 +4,8 @@ empty-cards-count-line = empty-cards-window-title = Empty Cards empty-cards-preserve-notes-checkbox = Keep notes with no valid cards empty-cards-delete-button = Delete +empty-cards-not-found = No empty cards. +empty-cards-deleted-count = Deleted { $cards -> + [one] { $cards } card. + *[other] { $cards } cards. + }