mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Expose new apkg and colpkg exporting
This commit is contained in:
parent
c237d4c969
commit
98d87fd36a
8 changed files with 262 additions and 8 deletions
|
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
######################################################################
|
||||
|
|
0
qt/aqt/import_export/__init__.py
Normal file
0
qt/aqt/import_export/__init__.py
Normal file
219
qt/aqt/import_export/exporting.py
Normal file
219
qt/aqt/import_export/exporting.py
Normal 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()
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
##########################################################################
|
||||
|
|
Loading…
Reference in a new issue