Expose new apkg and colpkg exporting

This commit is contained in:
RumovZ 2022-04-25 08:32:38 +02:00
parent c237d4c969
commit 98d87fd36a
8 changed files with 262 additions and 8 deletions

View file

@ -12,6 +12,7 @@ from anki import (
generic_pb2,
import_export_pb2,
links_pb2,
notes_pb2,
search_pb2,
stats_pb2,
)
@ -34,6 +35,9 @@ BrowserRow = search_pb2.BrowserRow
BrowserColumns = search_pb2.BrowserColumns
StripHtmlMode = card_rendering_pb2.StripHtmlRequest
ImportLogWithChanges = import_export_pb2.ImportAnkiPackageResponse
ExportAnkiPackageRequest = import_export_pb2.ExportAnkiPackageRequest
Empty = generic_pb2.Empty
NoteIds = notes_pb2.NoteIds
import copy
import os
@ -356,7 +360,7 @@ class Collection(DeprecatedNamesMixin):
"Throws if backup creation failed."
self._backend.await_backup_completion()
def export_collection(
def export_collection_package(
self, out_path: str, include_media: bool, legacy: bool
) -> None:
self.close_for_full_sync()
@ -367,6 +371,28 @@ class Collection(DeprecatedNamesMixin):
def import_anki_package(self, path: str) -> ImportLogWithChanges:
return self._backend.import_anki_package(package_path=path)
def export_anki_package(
self,
*,
out_path: str,
selector: DeckId | list[NoteId] | None,
with_scheduling: bool,
with_media: bool,
) -> int:
if selector is None:
selector_kwarg: dict[str, Any] = {"whole_collection": Empty()}
elif isinstance(selector, list):
selector_kwarg = {"note_ids": NoteIds(note_ids=selector)}
else:
selector_kwarg = {"deck_id": selector}
request = ExportAnkiPackageRequest(
out_path=out_path,
with_scheduling=with_scheduling,
with_media=with_media,
**selector_kwarg,
)
return self._backend.export_anki_package(request)
# Object helpers
##########################################################################

View file

@ -446,7 +446,7 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter):
time.sleep(0.1)
threading.Thread(target=progress).start()
self.col.export_collection(path, self.includeMedia, self.LEGACY)
self.col.export_collection_package(path, self.includeMedia, self.LEGACY)
class AnkiCollectionPackage21bExporter(AnkiCollectionPackageExporter):

View file

@ -23,7 +23,8 @@ from anki.tags import MARKED_TAG
from anki.utils import is_mac
from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor
from aqt.exporting import ExportDialog
from aqt.exporting import ExportDialog as LegacyExportDialog
from aqt.import_export.exporting import ExportDialog
from aqt.operations.card import set_card_deck, set_card_flag
from aqt.operations.collection import redo, undo
from aqt.operations.note import remove_notes
@ -792,8 +793,12 @@ class Browser(QMainWindow):
@no_arg_trigger
@skip_if_selection_is_empty
def _on_export_notes(self) -> None:
cids = self.selectedNotesAsCards()
ExportDialog(self.mw, cids=list(cids))
if os.getenv("ANKI_BACKEND_IMPORT_EXPORT"):
nids = self.selected_notes()
ExportDialog(self.mw, nids=nids)
else:
cids = self.selectedNotesAsCards()
LegacyExportDialog(self.mw, cids=list(cids))
# Flags & Marking
######################################################################

View file

View file

@ -0,0 +1,219 @@
# 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 os
import re
import time
from abc import ABC, abstractmethod
from concurrent.futures import Future
from dataclasses import dataclass
from typing import Sequence, Type
import aqt.forms
import aqt.main
from anki.decks import DeckId, DeckNameId
from anki.notes import NoteId
from aqt import gui_hooks
from aqt.errors import show_exception
from aqt.operations import QueryOpWithBackendProgress
from aqt.qt import *
from aqt.utils import (
checkInvalidFilename,
disable_help_button,
getSaveFile,
showWarning,
tooltip,
tr,
)
class ExportDialog(QDialog):
def __init__(
self,
mw: aqt.main.AnkiQt,
did: DeckId | None = None,
nids: Sequence[NoteId] | None = None,
):
QDialog.__init__(self, mw, Qt.WindowType.Window)
self.mw = mw
self.col = mw.col.weakref()
self.frm = aqt.forms.exporting.Ui_ExportDialog()
self.frm.setupUi(self)
self.exporter: Type[Exporter] = None
self.nids = nids
disable_help_button(self)
self.setup(did)
self.exec()
def setup(self, did: DeckId | None) -> None:
self.exporters: list[Type[Exporter]] = [ApkgExporter, ColpkgExporter]
self.frm.format.insertItems(0, [e.name() for e in self.exporters])
qconnect(self.frm.format.activated, self.exporter_changed)
self.exporter_changed(0)
# deck list
if self.nids is None:
self.all_decks = self.col.decks.all_names_and_ids()
decks = [tr.exporting_all_decks()]
decks.extend(d.name for d in self.all_decks)
else:
decks = [tr.exporting_selected_notes()]
self.frm.deck.addItems(decks)
# save button
b = QPushButton(tr.exporting_export())
self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole)
# set default option if accessed through deck button
if did:
name = self.mw.col.decks.get(did)["name"]
index = self.frm.deck.findText(name)
self.frm.deck.setCurrentIndex(index)
def exporter_changed(self, idx: int) -> None:
self.exporter = self.exporters[idx]
self.frm.includeSched.setVisible(self.exporter.show_include_scheduling)
self.frm.includeMedia.setVisible(self.exporter.show_include_media)
self.frm.includeTags.setVisible(self.exporter.show_include_tags)
self.frm.includeHTML.setVisible(self.exporter.show_include_html)
self.frm.deck.setVisible(self.exporter.show_deck_list)
def accept(self) -> None:
if not (out_path := self.get_out_path()):
return
self.exporter.export(self.mw, self.options(out_path))
QDialog.reject(self)
def get_out_path(self) -> str | None:
filename = self.filename()
while True:
path = getSaveFile(
parent=self,
title=tr.actions_export(),
dir_description="export",
key=self.exporter.name(),
ext=self.exporter.extension,
fname=filename,
)
if not path:
return None
if checkInvalidFilename(os.path.basename(path), dirsep=False):
continue
path = os.path.normpath(path)
if os.path.commonprefix([self.mw.pm.base, path]) == self.mw.pm.base:
showWarning("Please choose a different export location.")
continue
break
return path
def options(self, out_path: str) -> Options:
return Options(
out_path=out_path,
include_scheduling=self.frm.includeSched.isChecked(),
include_media=self.frm.includeMedia.isChecked(),
include_tags=self.frm.includeTags.isChecked(),
include_html=self.frm.includeHTML.isChecked(),
deck_id=self.current_deck_id(),
note_ids=self.nids,
)
def current_deck_id(self) -> DeckId | None:
return (deck := self.current_deck()) and DeckId(deck.id) or None
def current_deck(self) -> DeckNameId | None:
if self.exporter.show_deck_list:
if idx := self.frm.deck.currentIndex():
return self.all_decks[idx - 1]
return None
def filename(self) -> str:
if self.exporter.show_deck_list:
deck_name = self.frm.deck.currentText()
stem = re.sub('[\\\\/?<>:*|"^]', "_", deck_name)
else:
time_str = time.strftime("%Y-%m-%d@%H-%M-%S", time.localtime(time.time()))
stem = f"{tr.exporting_collection()}-{time_str}"
return f"{stem}.{self.exporter.extension}"
@dataclass
class Options:
out_path: str
include_scheduling: bool
include_media: bool
include_tags: bool
include_html: bool
deck_id: DeckId | None
note_ids: Sequence[NoteId] | None
class Exporter(ABC):
extension: str
show_deck_list = False
show_include_scheduling = False
show_include_media = False
show_include_tags = False
show_include_html = False
@staticmethod
@abstractmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
pass
@staticmethod
@abstractmethod
def name() -> str:
pass
class ColpkgExporter(Exporter):
extension = "colpkg"
show_include_media = True
@staticmethod
def name() -> str:
return tr.exporting_anki_collection_package()
@staticmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
def on_success(_future: Future) -> None:
mw.reopen()
tooltip(tr.exporting_collection_exported())
def on_failure(exception: Exception) -> None:
mw.reopen()
show_exception(parent=mw, exception=exception)
gui_hooks.collection_will_temporarily_close(mw.col)
QueryOpWithBackendProgress(
parent=mw,
op=lambda col: col.export_collection_package(
options.out_path, include_media=options.include_media, legacy=False
),
success=on_success,
key="exporting",
).failure(on_failure).run_in_background()
class ApkgExporter(Exporter):
extension = "apkg"
show_deck_list = True
show_include_scheduling = True
show_include_media = True
@staticmethod
def name() -> str:
return tr.exporting_anki_deck_package()
@staticmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
QueryOpWithBackendProgress(
parent=mw,
op=lambda col: col.export_anki_package(
out_path=options.out_path,
selector=options.note_ids or options.deck_id or None,
with_scheduling=options.include_scheduling,
with_media=options.include_media,
),
success=lambda fut: tooltip(tr.exporting_note_exported(count=fut)),
key="exporting",
).run_in_background()

View file

@ -13,7 +13,7 @@ import aqt.forms
import aqt.modelchooser
from anki.importing.anki2 import V2ImportIntoV1
from anki.importing.apkg import AnkiPackageImporter
from aqt.import_export import import_collection_package
from aqt.import_export.importing import import_collection_package
from aqt.main import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.utils import (

View file

@ -47,7 +47,8 @@ from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_
from aqt.dbcheck import check_db
from aqt.emptycards import show_empty_cards
from aqt.flags import FlagManager
from aqt.import_export import import_collection_package_op, import_file
from aqt.import_export.exporting import ExportDialog
from aqt.import_export.importing import import_collection_package_op, import_file
from aqt.legacy import install_pylib_legacy
from aqt.mediacheck import check_media_db
from aqt.mediasync import MediaSyncer
@ -1191,7 +1192,10 @@ title="{}" {}>{}</button>""".format(
def onExport(self, did: DeckId | None = None) -> None:
import aqt.exporting
aqt.exporting.ExportDialog(self, did=did)
if os.getenv("ANKI_BACKEND_IMPORT_EXPORT"):
ExportDialog(self, did=did)
else:
aqt.exporting.ExportDialog(self, did=did)
# Installing add-ons from CLI / mimetype handler
##########################################################################