From 0c80b5454fe8d163d2b6c86127cd8dac8cc7ecf5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 10 Sep 2017 16:58:55 +1000 Subject: [PATCH] use separate .colpkg extension for collection exports - allows translations of filename - allows users to keep multiple collection exports in the same folder - provides a clearer distinction between deck and collection packages - the collection/backup .apkg special cases will continue to work in future 2.1.x releases --- anki/exporting.py | 54 +++++++++++++---------- anki/importing/__init__.py | 2 +- aqt/exporting.py | 88 ++++++++++++++++++-------------------- aqt/importing.py | 7 ++- aqt/main.py | 6 +-- 5 files changed, 81 insertions(+), 76 deletions(-) diff --git a/anki/exporting.py b/anki/exporting.py index 43af05b15..535911593 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -45,7 +45,6 @@ class TextCardExporter(Exporter): key = _("Cards in Plain Text") ext = ".txt" - hideTags = True def __init__(self, col): Exporter.__init__(self, col) @@ -71,11 +70,11 @@ class TextNoteExporter(Exporter): key = _("Notes in Plain Text") ext = ".txt" + includeTags = True def __init__(self, col): Exporter.__init__(self, col) self.includeID = False - self.includeTags = True def doExport(self, file): cardIds = self.cardIds() @@ -107,11 +106,11 @@ class AnkiExporter(Exporter): key = _("Anki 2.0 Deck") ext = ".anki2" + includeSched = False + includeMedia = True def __init__(self, col): Exporter.__init__(self, col) - self.includeSched = False - self.includeMedia = True def exportInto(self, path): # create a new collection at the target @@ -258,17 +257,12 @@ class AnkiPackageExporter(AnkiExporter): def exportInto(self, path): # open a zip file z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, allowZip64=True) - # if all decks and scheduling included, full export - if self.includeSched and not self.did: - media = self.exportVerbatim(z) - else: - # otherwise, filter - media = self.exportFiltered(z, path) + media = self.doExport(z, path) # media map z.writestr("media", json.dumps(media)) z.close() - def exportFiltered(self, z, path): + def doExport(self, z, path): # export into the anki2 file colfile = path.replace(".apkg", ".anki2") AnkiExporter.exportInto(self, colfile) @@ -285,18 +279,6 @@ class AnkiPackageExporter(AnkiExporter): shutil.rmtree(path.replace(".apkg", ".media")) return media - def exportVerbatim(self, z): - # close our deck & write it into the zip file, and reopen - self.count = self.col.cardCount() - self.col.close() - z.write(self.col.path, "collection.anki2") - self.col.reopen() - # copy all media - if not self.includeMedia: - return {} - mdir = self.col.media.dir() - return self._exportMedia(z, os.listdir(mdir), mdir) - def _exportMedia(self, z, files, fdir): media = {} for c, file in enumerate(files): @@ -319,6 +301,31 @@ class AnkiPackageExporter(AnkiExporter): # is zipped up pass +# Collection package +###################################################################### + +class AnkiCollectionPackageExporter(AnkiPackageExporter): + + key = _("Anki Collection Package") + ext = ".colpkg" + verbatim = True + includeSched = None + + def __init__(self, col): + AnkiPackageExporter.__init__(self, col) + + def doExport(self, z, path): + # close our deck & write it into the zip file, and reopen + self.count = self.col.cardCount() + self.col.close() + z.write(self.col.path, "collection.anki2") + self.col.reopen() + # copy all media + if not self.includeMedia: + return {} + mdir = self.col.media.dir() + return self._exportMedia(z, os.listdir(mdir), mdir) + # Export modules ########################################################################## @@ -326,6 +333,7 @@ def exporters(): def id(obj): return ("%s (*%s)" % (obj.key, obj.ext), obj) exps = [ + id(AnkiCollectionPackageExporter), id(AnkiPackageExporter), id(TextNoteExporter), id(TextCardExporter), diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py index e4ee06633..65fdf15ce 100644 --- a/anki/importing/__init__.py +++ b/anki/importing/__init__.py @@ -12,7 +12,7 @@ from anki.lang import _ Importers = ( (_("Text separated by tabs or semicolons (*)"), TextImporter), - (_("Packaged Anki Deck (*.apkg *.zip)"), AnkiPackageImporter), + (_("Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)"), AnkiPackageImporter), (_("Mnemosyne 2.0 Deck (*.db)"), MnemosyneImporter), (_("Supermemo XML export (*.xml)"), SupermemoXmlImporter), (_("Pauker 1.8 Lesson (*.pau.gz)"), PaukerImporter), diff --git a/aqt/exporting.py b/aqt/exporting.py index 56115e340..edb83c815 100644 --- a/aqt/exporting.py +++ b/aqt/exporting.py @@ -11,7 +11,7 @@ from aqt.utils import getSaveFile, tooltip, showWarning, askUser, \ from anki.exporting import exporters from anki.hooks import addHook, remHook from anki.lang import ngettext - +import time class ExportDialog(QDialog): @@ -26,9 +26,19 @@ class ExportDialog(QDialog): self.exec_() def setup(self, did): - self.frm.format.insertItems(0, list(zip(*exporters()))[0]) + self.exporters = exporters() + # if a deck specified, start with .apkg type selected + idx = 0 + if did: + for c, (k,e) in enumerate(self.exporters): + if e.ext == ".apkg": + idx = c + break + self.frm.format.insertItems(0, [e[0] for e in self.exporters]) + self.frm.format.setCurrentIndex(idx) self.frm.format.activated.connect(self.exporterChanged) - self.exporterChanged(0) + self.exporterChanged(idx) + # deck list self.decks = [_("All Decks")] + sorted(self.col.decks.allNames()) self.frm.deck.addItems(self.decks) # save button @@ -41,14 +51,18 @@ class ExportDialog(QDialog): self.frm.deck.setCurrentIndex(index) def exporterChanged(self, idx): - self.exporter = exporters()[idx][1](self.col) - self.isApkg = hasattr(self.exporter, "includeSched") + self.exporter = self.exporters[idx][1](self.col) + self.isApkg = self.exporter.ext == ".apkg" + self.isVerbatim = getattr(self.exporter, "verbatim", False) self.isTextNote = hasattr(self.exporter, "includeTags") - self.hideTags = hasattr(self.exporter, "hideTags") - self.frm.includeSched.setVisible(self.isApkg) - self.frm.includeMedia.setVisible(self.isApkg) + self.frm.includeSched.setVisible( + getattr(self.exporter, "includeSched", None) is not None) + self.frm.includeMedia.setVisible( + getattr(self.exporter, "includeMedia", None) is not None) self.frm.includeTags.setVisible( - not self.isApkg and not self.hideTags) + getattr(self.exporter, "includeTags", None) is not None) + # show deck list? + self.frm.deck.setVisible(not self.isVerbatim) def accept(self): self.exporter.includeSched = ( @@ -62,40 +76,25 @@ class ExportDialog(QDialog): else: name = self.decks[self.frm.deck.currentIndex()] self.exporter.did = self.col.decks.id(name) - if (self.isApkg and self.exporter.includeSched and not - self.exporter.did): - verbatim = True - # it's a verbatim apkg export, so place on desktop instead of - # choosing file; use homedir if no desktop - usingHomedir = False - file = os.path.join(QStandardPaths.writableLocation( - QStandardPaths.DesktopLocation), "collection.apkg") - if not os.path.exists(os.path.dirname(file)): - usingHomedir = True - file = os.path.join(QStandardPaths.writableLocation( - QStandardPaths.HomeLocation), "collection.apkg") - if os.path.exists(file): - if usingHomedir: - question = _("%s already exists in your home directory. Overwrite it?") - else: - question = _("%s already exists on your desktop. Overwrite it?") - if not askUser(question % "collection.apkg"): - return + if self.isVerbatim: + name = time.strftime("-%Y-%m-%d@%H-%M-%S", + time.localtime(time.time())) + deck_name = _("collection")+name else: - verbatim = False # Get deck name and remove invalid filename characters deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) - filename = '{0}{1}'.format(deck_name, self.exporter.ext) - while 1: - file = getSaveFile(self, _("Export"), "export", - self.exporter.key, self.exporter.ext, - fname=filename) - if not file: - return - if checkInvalidFilename(os.path.basename(file), dirsep=False): - continue - break + + filename = '{0}{1}'.format(deck_name, self.exporter.ext) + while 1: + file = getSaveFile(self, _("Export"), "export", + self.exporter.key, self.exporter.ext, + fname=filename) + if not file: + return + if checkInvalidFilename(os.path.basename(file), dirsep=False): + continue + break self.hide() if file: self.mw.progress.start(immediate=True) @@ -113,15 +112,10 @@ class ExportDialog(QDialog): addHook("exportedMediaFiles", exportedMedia) self.exporter.exportInto(file) remHook("exportedMediaFiles", exportedMedia) - if verbatim: - if usingHomedir: - msg = _("A file called %s was saved in your home directory.") - else: - msg = _("A file called %s was saved on your desktop.") - msg = msg % "collection.apkg" - period = 5000 + period = 3000 + if self.isVerbatim: + msg = _("Collection exported.") else: - period = 3000 if self.isTextNote: msg = ngettext("%d note exported.", "%d notes exported.", self.exporter.count) % self.exporter.count diff --git a/aqt/importing.py b/aqt/importing.py index 08cd019d8..641c0bd04 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -365,14 +365,17 @@ with a different browser.""") def setupApkgImport(mw, importer): base = os.path.basename(importer.file).lower() - full = (base == "collection.apkg") or re.match("backup-.*\\.apkg", base) + full = ((base == "collection.apkg") or + re.match("backup-.*\\.apkg", base) or + base.endswith(".colpkg")) if not full: # adding return True backup = re.match("backup-.*\\.apkg", base) if not mw.restoringBackup and not askUser(_("""\ This will delete your existing collection and replace it with the data in \ -the file you're importing. Are you sure?"""), msgfunc=QMessageBox.warning): +the file you're importing. Are you sure?"""), msgfunc=QMessageBox.warning, + defaultno=True): return False # schedule replacement; don't do it immediately as we may have been # called as part of the startup routine diff --git a/aqt/main.py b/aqt/main.py index cef5d47df..70ec8b739 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -211,7 +211,7 @@ Replace your collection with an earlier backup?"""), def doOpen(path): self._openBackup(path) getFile(self.profileDiag, _("Revert to backup"), - cb=doOpen, filter="*.apkg", dir=self.pm.backupFolder()) + cb=doOpen, filter="*.colpkg", dir=self.pm.backupFolder()) def _openBackup(self, path): try: @@ -384,7 +384,7 @@ from the profile screen.")) path = self.pm.collectionPath() # do backup - fname = time.strftime("backup-%Y-%m-%d-%H.%M.%S.apkg", time.localtime(time.time())) + fname = time.strftime("backup-%Y-%m-%d-%H.%M.%S.colpkg", time.localtime(time.time())) newpath = os.path.join(dir, fname) data = open(path, "rb").read() b = self.BackupThread(newpath, data) @@ -394,7 +394,7 @@ from the profile screen.")) backups = [] for file in os.listdir(dir): # only look for new-style format - m = re.match("backup-\d{4}-\d{2}-.+.apkg", file) + m = re.match("backup-\d{4}-\d{2}-.+.colpkg", file) if not m: continue backups.append(file)