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")
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),

View file

@ -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),

View file

@ -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

View file

@ -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

View file

@ -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)