Add deck/collection export hooks (#1971)

* Add ExportFormat enum and use it in Exporter classes

* Add exporter hooks and call them from new exporters

* Fix filter argument order and add example to docstring

* Refactor: Avoid repeating ExportFormat

* Rename Options to ExportOptions for better namespacing in add-ons

* Add simplified legacy exporter hooks

Allows add-ons to be notified of exports when legacy handlers are enabled, without the need for monkey-patches.

* Switch away from ExportFormat, opting to pass exporter class/instance instead

* Consistently use exporter instances rather than classes

* Revert Exportdialog.exporters rename

* Revert "Revert Exportdialog.exporters rename"

This reverts commit 357a3aa859.
This commit is contained in:
Aristotelis 2022-07-22 04:45:47 +02:00 committed by GitHub
parent b9fd6688d2
commit 070c8ac735
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 26 deletions

View file

@ -196,6 +196,7 @@ class ExportDialog(QDialog):
else: else:
self.on_export_finished() self.on_export_finished()
gui_hooks.legacy_exporter_will_export(self.exporter)
if self.isVerbatim: if self.isVerbatim:
gui_hooks.collection_will_temporarily_close(self.mw.col) gui_hooks.collection_will_temporarily_close(self.mw.col)
self.mw.progress.start() self.mw.progress.start()
@ -213,6 +214,7 @@ class ExportDialog(QDialog):
msg = tr.exporting_note_exported(count=self.exporter.count) msg = tr.exporting_note_exported(count=self.exporter.count)
else: else:
msg = tr.exporting_card_exported(count=self.exporter.count) msg = tr.exporting_card_exported(count=self.exporter.count)
gui_hooks.legacy_exporter_did_export(self.exporter)
tooltip(msg, period=3000) tooltip(msg, period=3000)
QDialog.reject(self) QDialog.reject(self)

View file

@ -42,21 +42,21 @@ class ExportDialog(QDialog):
self.col = mw.col.weakref() self.col = mw.col.weakref()
self.frm = aqt.forms.exporting.Ui_ExportDialog() self.frm = aqt.forms.exporting.Ui_ExportDialog()
self.frm.setupUi(self) self.frm.setupUi(self)
self.exporter: Type[Exporter] = None self.exporter: Exporter
self.nids = nids self.nids = nids
disable_help_button(self) disable_help_button(self)
self.setup(did) self.setup(did)
self.open() self.open()
def setup(self, did: DeckId | None) -> None: def setup(self, did: DeckId | None) -> None:
self.exporters: list[Type[Exporter]] = [ self.exporter_classes: list[Type[Exporter]] = [
ApkgExporter, ApkgExporter,
ColpkgExporter, ColpkgExporter,
NoteCsvExporter, NoteCsvExporter,
CardCsvExporter, CardCsvExporter,
] ]
self.frm.format.insertItems( self.frm.format.insertItems(
0, [f"{e.name()} (.{e.extension})" for e in self.exporters] 0, [f"{e.name()} (.{e.extension})" for e in self.exporter_classes]
) )
qconnect(self.frm.format.activated, self.exporter_changed) qconnect(self.frm.format.activated, self.exporter_changed)
if self.nids is None and not did: if self.nids is None and not did:
@ -86,7 +86,7 @@ class ExportDialog(QDialog):
self.frm.includeSched.setChecked(False) self.frm.includeSched.setChecked(False)
def exporter_changed(self, idx: int) -> None: def exporter_changed(self, idx: int) -> None:
self.exporter = self.exporters[idx] self.exporter = self.exporter_classes[idx]()
self.frm.includeSched.setVisible(self.exporter.show_include_scheduling) self.frm.includeSched.setVisible(self.exporter.show_include_scheduling)
self.frm.includeMedia.setVisible(self.exporter.show_include_media) self.frm.includeMedia.setVisible(self.exporter.show_include_media)
self.frm.includeTags.setVisible(self.exporter.show_include_tags) self.frm.includeTags.setVisible(self.exporter.show_include_tags)
@ -125,14 +125,14 @@ class ExportDialog(QDialog):
break break
return path return path
def options(self, out_path: str) -> Options: def options(self, out_path: str) -> ExportOptions:
limit: ExportLimit = None limit: ExportLimit = None
if self.nids: if self.nids:
limit = NoteIdsLimit(self.nids) limit = NoteIdsLimit(self.nids)
elif current_deck_id := self.current_deck_id(): elif current_deck_id := self.current_deck_id():
limit = DeckIdLimit(current_deck_id) limit = DeckIdLimit(current_deck_id)
return Options( return ExportOptions(
out_path=out_path, out_path=out_path,
include_scheduling=self.frm.includeSched.isChecked(), include_scheduling=self.frm.includeSched.isChecked(),
include_media=self.frm.includeMedia.isChecked(), include_media=self.frm.includeMedia.isChecked(),
@ -165,7 +165,7 @@ class ExportDialog(QDialog):
@dataclass @dataclass
class Options: class ExportOptions:
out_path: str out_path: str
include_scheduling: bool include_scheduling: bool
include_media: bool include_media: bool
@ -190,9 +190,8 @@ class Exporter(ABC):
show_include_notetype = False show_include_notetype = False
show_include_guid = False show_include_guid = False
@staticmethod
@abstractmethod @abstractmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None: def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
pass pass
@staticmethod @staticmethod
@ -210,10 +209,12 @@ class ColpkgExporter(Exporter):
def name() -> str: def name() -> str:
return tr.exporting_anki_collection_package() return tr.exporting_anki_collection_package()
@staticmethod def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
def export(mw: aqt.main.AnkiQt, options: Options) -> None: options = gui_hooks.exporter_will_export(options, self)
def on_success(_: None) -> None: def on_success(_: None) -> None:
mw.reopen() mw.reopen()
gui_hooks.exporter_did_export(options, self)
tooltip(tr.exporting_collection_exported(), parent=mw) tooltip(tr.exporting_collection_exported(), parent=mw)
def on_failure(exception: Exception) -> None: def on_failure(exception: Exception) -> None:
@ -245,8 +246,13 @@ class ApkgExporter(Exporter):
def name() -> str: def name() -> str:
return tr.exporting_anki_deck_package() return tr.exporting_anki_deck_package()
@staticmethod def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
def export(mw: aqt.main.AnkiQt, options: Options) -> None: options = gui_hooks.exporter_will_export(options, self)
def on_success(count: int) -> None:
gui_hooks.exporter_did_export(options, self)
tooltip(tr.exporting_note_exported(count=count), parent=mw)
QueryOp( QueryOp(
parent=mw, parent=mw,
op=lambda col: col.export_anki_package( op=lambda col: col.export_anki_package(
@ -256,9 +262,7 @@ class ApkgExporter(Exporter):
with_media=options.include_media, with_media=options.include_media,
legacy_support=options.legacy_support, legacy_support=options.legacy_support,
), ),
success=lambda count: tooltip( success=on_success,
tr.exporting_note_exported(count=count), parent=mw
),
).with_backend_progress(export_progress_update).run_in_background() ).with_backend_progress(export_progress_update).run_in_background()
@ -275,8 +279,13 @@ class NoteCsvExporter(Exporter):
def name() -> str: def name() -> str:
return tr.exporting_notes_in_plain_text() return tr.exporting_notes_in_plain_text()
@staticmethod def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
def export(mw: aqt.main.AnkiQt, options: Options) -> None: options = gui_hooks.exporter_will_export(options, self)
def on_success(count: int) -> None:
gui_hooks.exporter_did_export(options, self)
tooltip(tr.exporting_note_exported(count=count), parent=mw)
QueryOp( QueryOp(
parent=mw, parent=mw,
op=lambda col: col.export_note_csv( op=lambda col: col.export_note_csv(
@ -288,9 +297,7 @@ class NoteCsvExporter(Exporter):
with_notetype=options.include_notetype, with_notetype=options.include_notetype,
with_guid=options.include_guid, with_guid=options.include_guid,
), ),
success=lambda count: tooltip( success=on_success,
tr.exporting_note_exported(count=count), parent=mw
),
).with_backend_progress(export_progress_update).run_in_background() ).with_backend_progress(export_progress_update).run_in_background()
@ -303,8 +310,13 @@ class CardCsvExporter(Exporter):
def name() -> str: def name() -> str:
return tr.exporting_cards_in_plain_text() return tr.exporting_cards_in_plain_text()
@staticmethod def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
def export(mw: aqt.main.AnkiQt, options: Options) -> None: options = gui_hooks.exporter_will_export(options, self)
def on_success(count: int) -> None:
gui_hooks.exporter_did_export(options, self)
tooltip(tr.exporting_card_exported(count=count), parent=mw)
QueryOp( QueryOp(
parent=mw, parent=mw,
op=lambda col: col.export_card_csv( op=lambda col: col.export_card_csv(
@ -312,9 +324,7 @@ class CardCsvExporter(Exporter):
limit=options.limit, limit=options.limit,
with_html=options.include_html, with_html=options.include_html,
), ),
success=lambda count: tooltip( success=on_success,
tr.exporting_card_exported(count=count), parent=mw
),
).with_backend_progress(export_progress_update).run_in_background() ).with_backend_progress(export_progress_update).run_in_background()

View file

@ -816,6 +816,46 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
`output` provides access to the unused/missing file lists and the text output that will be shown in the Check Media screen.""", `output` provides access to the unused/missing file lists and the text output that will be shown in the Check Media screen.""",
), ),
# Importing/exporting data
###################
Hook(
name="exporter_will_export",
args=[
"export_options: aqt.import_export.exporting.ExportOptions",
"exporter: aqt.import_export.exporting.Exporter",
],
return_type="aqt.import_export.exporting.ExportOptions",
doc="""Called before collection and deck exports.
Allows add-ons to be notified of impending deck exports and potentially
modify the export options. To perform the export unaltered, please return
`export_options` as is, e.g.:
def on_exporter_will_export(export_options: ExportOptions, exporter: Exporter):
if not isinstance(exporter, ApkgExporter):
return export_options
export_options.limit = ...
return export_options
""",
),
Hook(
name="exporter_did_export",
args=[
"export_options: aqt.import_export.exporting.ExportOptions",
"exporter: aqt.import_export.exporting.Exporter",
],
doc="""Called after collection and deck exports.""",
),
Hook(
name="legacy_exporter_will_export",
args=["legacy_exporter: anki.exporting.Exporter"],
doc="""Called before collection and deck exports performed by legacy exporters.""",
),
Hook(
name="legacy_exporter_did_export",
args=["legacy_exporter: anki.exporting.Exporter"],
doc="""Called after collection and deck exports performed by legacy exporters.""",
),
# Dialog Manager # Dialog Manager
################### ###################
Hook( Hook(