mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
hook the empty cards code up to the GUI
This commit is contained in:
parent
f637ac957d
commit
6e8860cafa
7 changed files with 248 additions and 50 deletions
|
@ -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
|
||||
##########################################################################
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
96
qt/aqt/emptycards.py
Normal file
96
qt/aqt/emptycards.py
Normal 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)
|
|
@ -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
|
||||
######################################################################
|
||||
|
|
128
qt/designer/emptycards.ui
Normal file
128
qt/designer/emptycards.ui
Normal 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>
|
|
@ -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.
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue