mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -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")
|
||||
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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue