mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
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:
parent
b9fd6688d2
commit
070c8ac735
3 changed files with 78 additions and 26 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue