mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32: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)
|
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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
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 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
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-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.
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue