mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
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:
parent
b454d6f169
commit
0c80b5454f
5 changed files with 81 additions and 76 deletions
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue