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 + + + + 0 + 0 + 423 + 22 + + + + + + false + + - - profiles - passEdit - login - add - rename - delete_2 - quit - - - - +