hook the empty cards code up to the GUI

This commit is contained in:
Damien Elmes 2020-04-25 19:44:48 +10:00
parent f637ac957d
commit 6e8860cafa
7 changed files with 248 additions and 50 deletions

View file

@ -533,26 +533,8 @@ select id from notes where id in %s and id not in (select nid from cards)"""
self._remNotes(nids) self._remNotes(nids)
def emptyCids(self) -> List[int]: def emptyCids(self) -> List[int]:
"""Returns IDs of empty cards.""" print("emptyCids() will go away")
rem: List[int] = [] return []
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
# Field checksums and sorting fields # Field checksums and sorting fields
########################################################################## ##########################################################################

View file

@ -700,6 +700,12 @@ class RustBackend:
except NotFoundError: except NotFoundError:
return None 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( def translate_string_in(
key: TR, **kwargs: Union[str, int, float] key: TR, **kwargs: Union[str, int, float]
) -> pb.TranslateStringIn: ) -> pb.TranslateStringIn:

View file

@ -72,7 +72,9 @@ def test_genrem():
t = m["tmpls"][1] t = m["tmpls"][1]
t["qfmt"] = "{{Back}}" t["qfmt"] = "{{Back}}"
mm.save(m, templates=True) 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 assert len(f.cards()) == 1
# if we add to the note, a card should be automatically generated # if we add to the note, a card should be automatically generated
f.load() f.load()

96
qt/aqt/emptycards.py Normal file
View file

@ -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+)\]",
"<a href=# onclick=\"pycmd('nid:\\1'); return false\">\\1</a>: ",
report.report,
)
style = "<style>.allempty { color: red; }</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)

View file

@ -10,6 +10,7 @@ import os
import re import re
import signal import signal
import time import time
import weakref
import zipfile import zipfile
from argparse import Namespace from argparse import Namespace
from concurrent.futures import Future 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 anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
from aqt import gui_hooks from aqt import gui_hooks
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user 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.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
@ -53,7 +55,6 @@ from aqt.utils import (
openLink, openLink,
restoreGeom, restoreGeom,
restoreState, restoreState,
saveGeom,
showInfo, showInfo,
showText, showText,
showWarning, showWarning,
@ -161,6 +162,10 @@ class AnkiQt(QMainWindow):
self.setupProfile() 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 # Profiles
########################################################################## ##########################################################################
@ -1336,34 +1341,8 @@ will be lost. Continue?"""
self.col.decks.select(self.col.decks.id(ret.name)) self.col.decks.select(self.col.decks.id(ret.name))
self.moveToState("overview") self.moveToState("overview")
def onEmptyCards(self): def onEmptyCards(self) -> None:
self.progress.start(immediate=True) show_empty_cards(self)
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()
# Debugging # Debugging
###################################################################### ######################################################################

128
qt/designer/emptycards.ui Normal file
View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>531</width>
<height>345</height>
</rect>
</property>
<property name="windowTitle">
<string>EMPTY_CARDS</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="AnkiWebView" name="webview" native="true">
<property name="url" stdset="0">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<widget class="QCheckBox" name="keep_notes">
<property name="text">
<string notr="true">KEEP_NOTES</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>AnkiWebView</class>
<extends>QWidget</extends>
<header location="global">aqt/webview</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -4,3 +4,8 @@ empty-cards-count-line =
empty-cards-window-title = Empty Cards empty-cards-window-title = Empty Cards
empty-cards-preserve-notes-checkbox = Keep notes with no valid cards empty-cards-preserve-notes-checkbox = Keep notes with no valid cards
empty-cards-delete-button = Delete empty-cards-delete-button = Delete
empty-cards-not-found = No empty cards.
empty-cards-deleted-count = Deleted { $cards ->
[one] { $cards } card.
*[other] { $cards } cards.
}