diff --git a/aqt/importing.py b/aqt/importing.py
index 24516108f..08cd019d8 100644
--- a/aqt/importing.py
+++ b/aqt/importing.py
@@ -370,24 +370,26 @@ def setupApkgImport(mw, importer):
# adding
return True
backup = re.match("backup-.*\\.apkg", base)
- if not askUser(_("""\
+ 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):
return False
# schedule replacement; don't do it immediately as we may have been
# called as part of the startup routine
- mw.progress.start(immediate=True)
mw.progress.timer(
100, lambda mw=mw, f=importer.file: replaceWithApkg(mw, f, backup), False)
def replaceWithApkg(mw, file, backup):
- # unload collection, which will also trigger a backup
- mw.unloadCollection()
+ mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
+
+def _replaceWithApkg(mw, file, backup):
+ mw.progress.start(immediate=True)
# overwrite collection
z = zipfile.ZipFile(file)
try:
z.extract("collection.anki2", mw.pm.profileFolder())
except:
+ mw.progress.finish()
showWarning(_("The provided file is not a valid .apkg file."))
return
# because users don't have a backup of media, it's safer to import new
@@ -408,7 +410,9 @@ def replaceWithApkg(mw, file, backup):
open(dest, "wb").write(data)
z.close()
# reload
- mw.loadCollection()
+ if not mw.loadCollection():
+ mw.progress.finish()
+ return
if backup:
mw.col.modSchema(check=False)
mw.progress.finish()
diff --git a/aqt/main.py b/aqt/main.py
index 4176e2994..471212627 100644
--- a/aqt/main.py
+++ b/aqt/main.py
@@ -24,8 +24,7 @@ import aqt.stats
import aqt.mediasrv
from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \
- openHelp, openLink, checkInvalidFilename
-import anki.db
+ openHelp, openLink, checkInvalidFilename, getFile
import sip
class AnkiQt(QMainWindow):
@@ -87,18 +86,29 @@ class AnkiQt(QMainWindow):
# Profiles
##########################################################################
+ class ProfileManager(QMainWindow):
+ onClose = pyqtSignal()
+ closeFires = True
+
+ def closeEvent(self, evt):
+ if self.closeFires:
+ self.onClose.emit()
+ evt.accept()
+
+ def closeWithoutQuitting(self):
+ self.closeFires = False
+ self.close()
+ self.closeFires = True
+
def setupProfile(self):
self.pendingImport = None
+ self.restoringBackup = False
# profile not provided on command line?
if not self.pm.name:
# if there's a single profile, load it automatically
profs = self.pm.profiles()
if len(profs) == 1:
- try:
- self.pm.load(profs[0])
- except:
- # password protected
- pass
+ self.pm.load(profs[0])
if not self.pm.name:
self.showProfileManager()
else:
@@ -107,20 +117,21 @@ class AnkiQt(QMainWindow):
def showProfileManager(self):
self.pm.profile = None
self.state = "profileManager"
- d = self.profileDiag = QDialog()
- f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
+ d = self.profileDiag = self.ProfileManager()
+ f = self.profileForm = aqt.forms.profiles.Ui_MainWindow()
f.setupUi(d)
f.login.clicked.connect(self.onOpenProfile)
f.profiles.itemDoubleClicked.connect(self.onOpenProfile)
- def onQuit():
- d.close()
- self.cleanupAndExit()
- f.quit.clicked.connect(onQuit)
+ f.openBackup.clicked.connect(self.onOpenBackup)
+ f.quit.clicked.connect(d.close)
+ d.onClose.connect(self.cleanupAndExit)
f.add.clicked.connect(self.onAddProfile)
f.rename.clicked.connect(self.onRenameProfile)
f.delete_2.clicked.connect(self.onRemProfile)
- d.rejected.connect(d.close)
f.profiles.currentRowChanged.connect(self.onProfileRowChange)
+ f.statusbar.setVisible(False)
+ # enter key opens profile
+ QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile)
self.refreshProfilesList()
# raise first, for osx testing
d.show()
@@ -142,22 +153,14 @@ class AnkiQt(QMainWindow):
return
name = self.pm.profiles()[n]
f = self.profileForm
- passwd = not self.pm.load(name)
- f.passEdit.setVisible(passwd)
- f.passLabel.setVisible(passwd)
+ self.pm.load(name)
def openProfile(self):
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
- passwd = self.profileForm.passEdit.text()
- return self.pm.load(name, passwd)
+ return self.pm.load(name)
def onOpenProfile(self):
- if not self.openProfile():
- showWarning(_("Invalid password."))
- return
- self.profileDiag.close()
- self.loadProfile()
- return True
+ self.loadProfile(self.profileDiag.closeWithoutQuitting)
def profileNameOk(self, str):
return not checkInvalidFilename(str)
@@ -176,8 +179,6 @@ class AnkiQt(QMainWindow):
def onRenameProfile(self):
name = getOnlyText(_("New name:"), default=self.pm.name)
- if not self.openProfile():
- return showWarning(_("Invalid password."))
if not name:
return
if name == self.pm.name:
@@ -193,18 +194,43 @@ class AnkiQt(QMainWindow):
profs = self.pm.profiles()
if len(profs) < 2:
return showWarning(_("There must be at least one profile."))
- # password correct?
- if not self.openProfile():
- return
# sure?
if not askUser(_("""\
All cards, notes, and media for this profile will be deleted. \
-Are you sure?""")):
+Are you sure?"""), msgfunc=QMessageBox.warning, defaultno=True):
return
self.pm.remove(self.pm.name)
self.refreshProfilesList()
- def loadProfile(self):
+ def onOpenBackup(self):
+ if not askUser(_("""\
+Replace your collection with an earlier backup?"""),
+ msgfunc=QMessageBox.warning,
+ defaultno=True):
+ return
+ def doOpen(path):
+ self._openBackup(path)
+ getFile(self.profileDiag, _("Revert to backup"),
+ cb=doOpen, filter="*.apkg", dir=self.pm.backupFolder())
+
+ def _openBackup(self, path):
+ try:
+ # move the existing collection to the trash, as it may not open
+ self.pm.trashCollection()
+ except:
+ showWarning(_("Unable to move existing file to trash - please try restarting your computer."))
+ return
+
+ self.pendingImport = path
+ self.restoringBackup = True
+
+ showInfo(_("""\
+Automatic syncing and backups have been disabled while restoring. To enable them again, \
+close the profile or restart Anki."""))
+
+ self.onOpenProfile()
+
+ def loadProfile(self, onsuccess=None):
self.maybeAutoSync()
if not self.loadCollection():
@@ -223,14 +249,11 @@ Are you sure?""")):
# import pending?
if self.pendingImport:
- if self.pm.profile['key']:
- showInfo(_("""\
-To import into a password protected profile, please open the profile before attempting to import."""))
- else:
- self.handleImport(self.pendingImport)
-
+ self.handleImport(self.pendingImport)
self.pendingImport = None
runHook("profileLoaded")
+ if onsuccess:
+ onsuccess()
def unloadProfile(self, onsuccess):
def callback():
@@ -246,12 +269,13 @@ To import into a password protected profile, please open the profile before atte
self.pm.save()
self.hide()
+ self.restoringBackup = False
+
# at this point there should be no windows left
self._checkForUnclosedWidgets()
self.maybeAutoSync()
- self.pm.profile = None
def _checkForUnclosedWidgets(self):
for w in self.app.topLevelWidgets():
@@ -279,8 +303,8 @@ To import into a password protected profile, please open the profile before atte
except Exception as e:
showWarning(_("""\
Anki was unable to open your collection file. If problems persist after \
-restarting your computer, please see the manual for how to restore from \
-an automatic backup.
+restarting your computer, please use the Open Backup button in the profile \
+manager.
Debug info:
""")+traceback.format_exc())
@@ -300,7 +324,13 @@ Debug info:
self.closeAllWindows(callback)
def _unloadCollection(self):
- self.progress.start(label=_("Backing Up..."), immediate=True)
+ if not self.col:
+ return
+ if self.restoringBackup:
+ label = _("Closing...")
+ else:
+ label = _("Backing Up...")
+ self.progress.start(label=label, immediate=True)
corrupt = False
try:
self.maybeOptimize()
@@ -315,11 +345,11 @@ when the collection is stored on a network or cloud drive. Please see \
the manual for information on how to restore from an automatic backup."))
try:
self.col.close()
- except:
- pass
- self.col = None
- if not corrupt:
+ finally:
+ self.col = None
+ if not corrupt and not self.restoringBackup:
self.backup()
+
self.progress.finish()
# Backup and auto-optimize
@@ -534,7 +564,7 @@ title="%s" %s>%s''' % (
self.form.centralwidget.setLayout(self.mainLayout)
def closeAllWindows(self, onsuccess):
- return aqt.dialogs.closeAll(onsuccess)
+ aqt.dialogs.closeAll(onsuccess)
# Components
##########################################################################
@@ -544,7 +574,8 @@ title="%s" %s>%s''' % (
def onSigInt(self, signum, frame):
# interrupt any current transaction and schedule a rollback & quit
- self.col.db.interrupt()
+ if self.col:
+ self.col.db.interrupt()
def quit():
self.col.db.rollback()
self.close()
@@ -596,10 +627,8 @@ title="%s" %s>%s''' % (
def maybeAutoSync(self):
if (not self.pm.profile['syncKey']
or not self.pm.profile['autoSync']
- or self.safeMode):
- return
- if self.pendingImport and os.path.basename(
- self.pendingImport).startswith("backup-"):
+ or self.safeMode
+ or self.restoringBackup):
return
# ok to sync
@@ -669,10 +698,15 @@ title="%s" %s>%s''' % (
##########################################################################
def closeEvent(self, event):
- "User hit the X button, etc."
- # ignore the event for now, as we need time to clean up
- event.ignore()
- self.unloadProfileAndExit()
+ if self.state == "profileManager":
+ # if profile manager active, this event may fire via OS X menu bar's
+ # quit option
+ self.profileDiag.close()
+ event.accept()
+ else:
+ # ignore the event for now, as we need time to clean up
+ event.ignore()
+ self.unloadProfileAndExit()
# Undo & autosave
##########################################################################
diff --git a/aqt/preferences.py b/aqt/preferences.py
index b79af4a8b..ba3342fdb 100644
--- a/aqt/preferences.py
+++ b/aqt/preferences.py
@@ -150,22 +150,7 @@ Not currently enabled; click the sync button in the main window to enable."""))
def setupOptions(self):
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
- self.form.profilePass.clicked.connect(self.onProfilePass)
def updateOptions(self):
self.prof['pastePNG'] = self.form.pastePNG.isChecked()
- def onProfilePass(self):
- pw, ret = getText(_("""\
-Lock account with password, or leave blank:"""))
- if not ret:
- return
- if not pw:
- self.prof['key'] = None
- return
- pw2, ret = getText(_("Confirm password:"))
- if not ret:
- return
- if pw != pw2:
- showWarning(_("Passwords didn't match"))
- self.prof['key'] = self.mw.pm._pwhash(pw)
diff --git a/aqt/profiles.py b/aqt/profiles.py
index 5fcfea366..fa08a722a 100644
--- a/aqt/profiles.py
+++ b/aqt/profiles.py
@@ -37,7 +37,6 @@ metaConf = dict(
profileConf = dict(
# profile
- key=None,
mainWindowGeom=None,
mainWindowState=None,
numBackups=30,
@@ -133,13 +132,10 @@ a flash drive.""" % self.base)
self.db.list("select name from profiles")
if x != "_global")
- def load(self, name, passwd=None):
+ def load(self, name):
data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name)
# some profiles created in python2 may not decode properly
prof = pickle.loads(data, errors="ignore")
- if prof['key'] and prof['key'] != self._pwhash(passwd):
- self.name = None
- return False
if name != "_global":
self.name = name
self.profile = prof
@@ -164,6 +160,11 @@ a flash drive.""" % self.base)
self.db.execute("delete from profiles where name = ?", name)
self.db.commit()
+ def trashCollection(self):
+ p = self.collectionPath()
+ if os.path.exists(p):
+ send2trash(p)
+
def rename(self, name):
oldName = self.name
oldFolder = self.profileFolder()
@@ -319,9 +320,6 @@ please see:
%s
""") % (appHelpSite + "#startupopts"))
- def _pwhash(self, passwd):
- return checksum(str(self.meta['id'])+str(passwd))
-
# Default language
######################################################################
# On first run, allow the user to choose the default language
diff --git a/designer/preferences.ui b/designer/preferences.ui
index 682cab961..bfe3424c5 100644
--- a/designer/preferences.ui
+++ b/designer/preferences.ui
@@ -184,16 +184,6 @@
- -
-
-
- Profile Password...
-
-
- false
-
-
-
@@ -429,7 +419,6 @@
dayOffset
lrnCutoff
timeLimit
- profilePass
syncMedia
syncOnProgramOpen
syncDeauth
diff --git a/designer/profiles.ui b/designer/profiles.ui
index 8c907df33..4ec6dba88 100644
--- a/designer/profiles.ui
+++ b/designer/profiles.ui
@@ -1,120 +1,114 @@
- Dialog
-
+ MainWindow
+
0
0
- 352
- 283
+ 423
+ 356
Profiles
-
-
- :/icons/anki.png:/icons/anki.png
-
-
- -
-
-
- Profile:
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
- Password:
-
-
-
- -
-
-
- QLineEdit::Password
-
-
-
-
-
- -
-
-
-
-
-
- Open
-
-
-
- -
-
-
- Add
-
-
-
- -
-
-
- Rename
-
-
-
- -
-
-
- Delete
-
-
-
- -
-
-
- Quit
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
-
+
+
+ -
+
+
-
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+ Open
+
+
+ true
+
+
+
+ -
+
+
+ Add
+
+
+
+ -
+
+
+ Rename
+
+
+
+ -
+
+
+ Delete
+
+
+
+ -
+
+
+ Quit
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Open Backup...
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
-
- profiles
- passEdit
- login
- add
- rename
- delete_2
- quit
-
-
-
-
+