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
This commit is contained in:
Damien Elmes 2017-09-10 16:58:55 +10:00
parent b454d6f169
commit 0c80b5454f
5 changed files with 81 additions and 76 deletions

View file

@ -45,7 +45,6 @@ class TextCardExporter(Exporter):
key = _("Cards in Plain Text") key = _("Cards in Plain Text")
ext = ".txt" ext = ".txt"
hideTags = True
def __init__(self, col): def __init__(self, col):
Exporter.__init__(self, col) Exporter.__init__(self, col)
@ -71,11 +70,11 @@ class TextNoteExporter(Exporter):
key = _("Notes in Plain Text") key = _("Notes in Plain Text")
ext = ".txt" ext = ".txt"
includeTags = True
def __init__(self, col): def __init__(self, col):
Exporter.__init__(self, col) Exporter.__init__(self, col)
self.includeID = False self.includeID = False
self.includeTags = True
def doExport(self, file): def doExport(self, file):
cardIds = self.cardIds() cardIds = self.cardIds()
@ -107,11 +106,11 @@ class AnkiExporter(Exporter):
key = _("Anki 2.0 Deck") key = _("Anki 2.0 Deck")
ext = ".anki2" ext = ".anki2"
includeSched = False
includeMedia = True
def __init__(self, col): def __init__(self, col):
Exporter.__init__(self, col) Exporter.__init__(self, col)
self.includeSched = False
self.includeMedia = True
def exportInto(self, path): def exportInto(self, path):
# create a new collection at the target # create a new collection at the target
@ -258,17 +257,12 @@ class AnkiPackageExporter(AnkiExporter):
def exportInto(self, path): def exportInto(self, path):
# open a zip file # open a zip file
z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, allowZip64=True) z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, allowZip64=True)
# if all decks and scheduling included, full export media = self.doExport(z, path)
if self.includeSched and not self.did:
media = self.exportVerbatim(z)
else:
# otherwise, filter
media = self.exportFiltered(z, path)
# media map # media map
z.writestr("media", json.dumps(media)) z.writestr("media", json.dumps(media))
z.close() z.close()
def exportFiltered(self, z, path): def doExport(self, z, path):
# export into the anki2 file # export into the anki2 file
colfile = path.replace(".apkg", ".anki2") colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile) AnkiExporter.exportInto(self, colfile)
@ -285,18 +279,6 @@ class AnkiPackageExporter(AnkiExporter):
shutil.rmtree(path.replace(".apkg", ".media")) shutil.rmtree(path.replace(".apkg", ".media"))
return 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): def _exportMedia(self, z, files, fdir):
media = {} media = {}
for c, file in enumerate(files): for c, file in enumerate(files):
@ -319,6 +301,31 @@ class AnkiPackageExporter(AnkiExporter):
# is zipped up # is zipped up
pass 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 # Export modules
########################################################################## ##########################################################################
@ -326,6 +333,7 @@ def exporters():
def id(obj): def id(obj):
return ("%s (*%s)" % (obj.key, obj.ext), obj) return ("%s (*%s)" % (obj.key, obj.ext), obj)
exps = [ exps = [
id(AnkiCollectionPackageExporter),
id(AnkiPackageExporter), id(AnkiPackageExporter),
id(TextNoteExporter), id(TextNoteExporter),
id(TextCardExporter), id(TextCardExporter),

View file

@ -12,7 +12,7 @@ from anki.lang import _
Importers = ( Importers = (
(_("Text separated by tabs or semicolons (*)"), TextImporter), (_("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), (_("Mnemosyne 2.0 Deck (*.db)"), MnemosyneImporter),
(_("Supermemo XML export (*.xml)"), SupermemoXmlImporter), (_("Supermemo XML export (*.xml)"), SupermemoXmlImporter),
(_("Pauker 1.8 Lesson (*.pau.gz)"), PaukerImporter), (_("Pauker 1.8 Lesson (*.pau.gz)"), PaukerImporter),

View file

@ -11,7 +11,7 @@ from aqt.utils import getSaveFile, tooltip, showWarning, askUser, \
from anki.exporting import exporters from anki.exporting import exporters
from anki.hooks import addHook, remHook from anki.hooks import addHook, remHook
from anki.lang import ngettext from anki.lang import ngettext
import time
class ExportDialog(QDialog): class ExportDialog(QDialog):
@ -26,9 +26,19 @@ class ExportDialog(QDialog):
self.exec_() self.exec_()
def setup(self, did): 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.frm.format.activated.connect(self.exporterChanged)
self.exporterChanged(0) self.exporterChanged(idx)
# deck list
self.decks = [_("All Decks")] + sorted(self.col.decks.allNames()) self.decks = [_("All Decks")] + sorted(self.col.decks.allNames())
self.frm.deck.addItems(self.decks) self.frm.deck.addItems(self.decks)
# save button # save button
@ -41,14 +51,18 @@ class ExportDialog(QDialog):
self.frm.deck.setCurrentIndex(index) self.frm.deck.setCurrentIndex(index)
def exporterChanged(self, idx): def exporterChanged(self, idx):
self.exporter = exporters()[idx][1](self.col) self.exporter = self.exporters[idx][1](self.col)
self.isApkg = hasattr(self.exporter, "includeSched") self.isApkg = self.exporter.ext == ".apkg"
self.isVerbatim = getattr(self.exporter, "verbatim", False)
self.isTextNote = hasattr(self.exporter, "includeTags") self.isTextNote = hasattr(self.exporter, "includeTags")
self.hideTags = hasattr(self.exporter, "hideTags") self.frm.includeSched.setVisible(
self.frm.includeSched.setVisible(self.isApkg) getattr(self.exporter, "includeSched", None) is not None)
self.frm.includeMedia.setVisible(self.isApkg) self.frm.includeMedia.setVisible(
getattr(self.exporter, "includeMedia", None) is not None)
self.frm.includeTags.setVisible( 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): def accept(self):
self.exporter.includeSched = ( self.exporter.includeSched = (
@ -62,40 +76,25 @@ class ExportDialog(QDialog):
else: else:
name = self.decks[self.frm.deck.currentIndex()] name = self.decks[self.frm.deck.currentIndex()]
self.exporter.did = self.col.decks.id(name) self.exporter.did = self.col.decks.id(name)
if (self.isApkg and self.exporter.includeSched and not if self.isVerbatim:
self.exporter.did): name = time.strftime("-%Y-%m-%d@%H-%M-%S",
verbatim = True time.localtime(time.time()))
# it's a verbatim apkg export, so place on desktop instead of deck_name = _("collection")+name
# 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
else: else:
verbatim = False
# Get deck name and remove invalid filename characters # Get deck name and remove invalid filename characters
deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = self.decks[self.frm.deck.currentIndex()]
deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name)
filename = '{0}{1}'.format(deck_name, self.exporter.ext)
while 1: filename = '{0}{1}'.format(deck_name, self.exporter.ext)
file = getSaveFile(self, _("Export"), "export", while 1:
self.exporter.key, self.exporter.ext, file = getSaveFile(self, _("Export"), "export",
fname=filename) self.exporter.key, self.exporter.ext,
if not file: fname=filename)
return if not file:
if checkInvalidFilename(os.path.basename(file), dirsep=False): return
continue if checkInvalidFilename(os.path.basename(file), dirsep=False):
break continue
break
self.hide() self.hide()
if file: if file:
self.mw.progress.start(immediate=True) self.mw.progress.start(immediate=True)
@ -113,15 +112,10 @@ class ExportDialog(QDialog):
addHook("exportedMediaFiles", exportedMedia) addHook("exportedMediaFiles", exportedMedia)
self.exporter.exportInto(file) self.exporter.exportInto(file)
remHook("exportedMediaFiles", exportedMedia) remHook("exportedMediaFiles", exportedMedia)
if verbatim: period = 3000
if usingHomedir: if self.isVerbatim:
msg = _("A file called %s was saved in your home directory.") msg = _("Collection exported.")
else:
msg = _("A file called %s was saved on your desktop.")
msg = msg % "collection.apkg"
period = 5000
else: else:
period = 3000
if self.isTextNote: if self.isTextNote:
msg = ngettext("%d note exported.", "%d notes exported.", msg = ngettext("%d note exported.", "%d notes exported.",
self.exporter.count) % self.exporter.count self.exporter.count) % self.exporter.count

View file

@ -365,14 +365,17 @@ with a different browser.""")
def setupApkgImport(mw, importer): def setupApkgImport(mw, importer):
base = os.path.basename(importer.file).lower() 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: if not full:
# adding # adding
return True return True
backup = re.match("backup-.*\\.apkg", base) backup = re.match("backup-.*\\.apkg", base)
if not mw.restoringBackup and not askUser(_("""\ if not mw.restoringBackup and not askUser(_("""\
This will delete your existing collection and replace it with the data in \ 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 return False
# schedule replacement; don't do it immediately as we may have been # schedule replacement; don't do it immediately as we may have been
# called as part of the startup routine # called as part of the startup routine

View file

@ -211,7 +211,7 @@ Replace your collection with an earlier backup?"""),
def doOpen(path): def doOpen(path):
self._openBackup(path) self._openBackup(path)
getFile(self.profileDiag, _("Revert to backup"), 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): def _openBackup(self, path):
try: try:
@ -384,7 +384,7 @@ from the profile screen."))
path = self.pm.collectionPath() path = self.pm.collectionPath()
# do backup # 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) newpath = os.path.join(dir, fname)
data = open(path, "rb").read() data = open(path, "rb").read()
b = self.BackupThread(newpath, data) b = self.BackupThread(newpath, data)
@ -394,7 +394,7 @@ from the profile screen."))
backups = [] backups = []
for file in os.listdir(dir): for file in os.listdir(dir):
# only look for new-style format # 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: if not m:
continue continue
backups.append(file) backups.append(file)