upgrade wizard and first startup language selection dialog

This commit is contained in:
Damien Elmes 2011-11-25 15:08:41 +09:00
parent 7c68b58d44
commit a1a7e7341c
4 changed files with 379 additions and 5 deletions

View file

@ -30,6 +30,18 @@ class AnkiQt(QMainWindow):
aqt.mw = self aqt.mw = self
self.app = app self.app = app
self.pm = profileManager self.pm = profileManager
# use the global language for early init; once a profile is loaded we
# can switch to a user's preferred language
self.setupLang(force=self.pm.meta['defaultLang'])
# running 2.0 for the first time?
if self.pm.meta['firstRun']:
# upgrade if necessary
from aqt.upgrade import Upgrader
u = Upgrader(self)
u.maybeUpgrade()
self.pm.meta['firstRun'] = False
self.pm.save()
# init rest of app
try: try:
self.setupUI() self.setupUI()
self.setupAddons() self.setupAddons()
@ -41,7 +53,6 @@ class AnkiQt(QMainWindow):
def setupUI(self): def setupUI(self):
self.col = None self.col = None
self.state = None self.state = None
self.setupLang("en") # bootstrap with english; profile will adjust
self.setupThreads() self.setupThreads()
self.setupMainWindow() self.setupMainWindow()
self.setupStyle() self.setupStyle()
@ -67,7 +78,7 @@ class AnkiQt(QMainWindow):
def setupProfile(self): def setupProfile(self):
# profile not provided on command line? # profile not provided on command line?
if False: # not self.pm.name: if not self.pm.name:
# if there's a single profile, load it automatically # if there's a single profile, load it automatically
profs = self.pm.profiles() profs = self.pm.profiles()
if len(profs) == 1: if len(profs) == 1:
@ -657,7 +668,7 @@ Please choose a new deck name:"""))
########################################################################## ##########################################################################
def setupLang(self, force=None): def setupLang(self, force=None):
"Set the user interface language." "Set the user interface language for the current profile."
import locale, gettext import locale, gettext
import anki.lang import anki.lang
try: try:

View file

@ -7,9 +7,11 @@
# - Saves in sqlite rather than a flat file so the config can't be corrupted # - Saves in sqlite rather than a flat file so the config can't be corrupted
from aqt.qt import * from aqt.qt import *
import os, sys, time, random, cPickle, shutil import os, sys, time, random, cPickle, shutil, locale, re
from anki.db import DB from anki.db import DB
from anki.utils import isMac, isWin, intTime, checksum from anki.utils import isMac, isWin, intTime, checksum
from anki.lang import langs
import aqt.forms
metaConf = dict( metaConf = dict(
ver=0, ver=0,
@ -19,6 +21,7 @@ metaConf = dict(
lastMsg=-1, lastMsg=-1,
suppressUpdate=False, suppressUpdate=False,
firstRun=True, firstRun=True,
defaultLang=None,
) )
profileConf = dict( profileConf = dict(
@ -103,8 +106,10 @@ documentation for information on using a flash drive.""")
self.db.commit() self.db.commit()
def create(self, name): def create(self, name):
prof = profileConf.copy()
prof['lang'] = self.meta['defaultLang']
self.db.execute("insert into profiles values (?, ?)", self.db.execute("insert into profiles values (?, ?)",
name, cPickle.dumps(profileConf)) name, cPickle.dumps(prof))
self.db.commit() self.db.commit()
def remove(self, name): def remove(self, name):
@ -159,6 +164,7 @@ create table if not exists profiles
self.meta = metaConf.copy() self.meta = metaConf.copy()
self.db.execute("insert into profiles values ('_global', ?)", self.db.execute("insert into profiles values ('_global', ?)",
cPickle.dumps(metaConf)) cPickle.dumps(metaConf))
self._setDefaultLang()
# and save a default user profile for later (commits) # and save a default user profile for later (commits)
self.create("User 1") self.create("User 1")
else: else:
@ -169,3 +175,50 @@ create table if not exists profiles
def _pwhash(self, passwd): def _pwhash(self, passwd):
return checksum(unicode(self.meta['id'])+unicode(passwd)) return checksum(unicode(self.meta['id'])+unicode(passwd))
# Default language
######################################################################
# On first run, allow the user to choose the default language
def _setDefaultLang(self):
# the dialog expects _ to be defined, but we're running before
# setLang() has been called. so we create a dummy op for now
import __builtin__
__builtin__.__dict__['_'] = lambda x: x
# create dialog
class NoCloseDiag(QDialog):
def reject(self):
pass
d = self.langDiag = NoCloseDiag()
f = self.langForm = aqt.forms.setlang.Ui_Dialog()
f.setupUi(d)
d.connect(d, SIGNAL("accepted()"), self._onLangSelected)
d.connect(d, SIGNAL("rejected()"), lambda: True)
# default to the system language
(lang, enc) = locale.getdefaultlocale()
if lang and lang not in ("pt_BR", "zh_CN", "zh_TW"):
lang = re.sub("(.*)_.*", "\\1", lang)
# find index
idx = None
en = None
for c, (name, code) in enumerate(langs):
if code == "en":
en = c
if code == lang:
idx = c
# if the system language isn't available, revert to english
if idx is None:
idx = en
# update list
f.lang.addItems([x[0] for x in langs])
f.lang.setCurrentRow(idx)
d.exec_()
def _onLangSelected(self):
f = self.langForm
code = langs[f.lang.currentRow()][1]
self.meta['defaultLang'] = code
sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, cPickle.dumps(self.meta), "_global")
self.db.commit()

236
aqt/upgrade.py Normal file
View file

@ -0,0 +1,236 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os, cPickle
from aqt.qt import *
from anki.utils import isMac, isWin
from anki import Collection
from anki.importing import Anki1Importer
class Upgrader(object):
def __init__(self, mw):
self.mw = mw
def maybeUpgrade(self):
p = self._oldConfigPath()
# does an old config file exist?
if not os.path.exists(p):
return
# load the new deck user profile
self.mw.pm.load(self.mw.pm.profiles()[0])
# load old settings and copy over
self._loadConf(p)
self._copySettings()
# and show the wizard
self._showWizard()
# Settings
######################################################################
def _oldConfigPath(self):
if isWin:
p = "~/.anki/config.db"
elif isMac:
p = "~/Library/Application Support/Anki/config.db"
else:
p = "~/.anki/config.db"
return os.path.expanduser(p)
def _loadConf(self, path):
self.conf = cPickle.load(open(path))
def _copySettings(self):
p = self.mw.pm.profile
for k in (
"recentColours", "stripHTML", "editFontFamily", "editFontSize",
"editLineSize", "deleteMedia", "preserveKeyboard", "numBackups",
"proxyHost", "proxyPass", "proxyPort", "proxyUser",
"showProgress"):
p[k] = self.conf[k]
p['autoplay'] = self.conf['autoplaySounds']
p['showDueTimes'] = not self.conf['suppressEstimates']
self.mw.pm.save()
# Wizard
######################################################################
def _showWizard(self):
class Wizard(QWizard):
def reject(self):
pass
self.wizard = w = Wizard()
w.addPage(self._welcomePage())
w.addPage(self._decksPage())
w.addPage(self._mediaPage())
w.addPage(self._readyPage())
w.addPage(self._upgradePage())
w.addPage(self._finishedPage())
w.setWindowTitle("Upgrade Wizard")
w.setWizardStyle(QWizard.ModernStyle)
w.exec_()
def _labelPage(self, title, txt):
p = QWizardPage()
p.setTitle(title)
l = QLabel(txt)
l.setTextFormat(Qt.RichText)
l.setTextInteractionFlags(Qt.TextSelectableByMouse)
l.setWordWrap(True)
v = QVBoxLayout()
v.addWidget(l)
p.setLayout(v)
return p
def _welcomePage(self):
return self._labelPage(_("Welcome"), _("""\
This wizard will guide you through the Anki 2.0 upgrade process.
For a smooth upgrade, please read the following pages carefully.
"""))
def _decksPage(self):
return self._labelPage(_("Your Decks"), _("""\
Anki 2 stores your decks in a new format. This wizard will automatically
convert your decks to that format. Your decks will be backed up before
the upgrade, so if you need to revert to the previous version of Anki, your
decks will still be usable."""))
def _mediaPage(self):
return self._labelPage(_("Sounds & Images"), _("""\
When your decks are upgraded, Anki will attempt to copy any sounds and images
from the old decks. If you were using a custom DropBox folder or custom media
folder, the upgrade process may not be able to locate your media. Later on, a
report of the upgrade will be presented to you. If you notice media was not
copied when it should have been, please see the upgrade guide for more
instructions.
<p>
AnkiWeb now supports media syncing directly. No special setup is required, and
media will be synchronized along with your cards when you sync to AnkiWeb."""))
def _readyPage(self):
class ReadyPage(QWizardPage):
def initializePage(self):
self.setTitle(_("Ready to Upgrade"))
self.setCommitPage(True)
l = QLabel(_("""\
When you're ready to upgrade, click the commit button to continue. The upgrade
guide will open in your browser while the upgrade proceeds. Please read it
carefully, as a lot has changed since the previous Anki version."""))
l.setTextFormat(Qt.RichText)
l.setTextInteractionFlags(Qt.TextSelectableByMouse)
l.setWordWrap(True)
v = QVBoxLayout()
v.addWidget(l)
self.setLayout(v)
return ReadyPage()
def _upgradePage(self):
decks = self.conf['recentDeckPaths']
colpath = self.mw.pm.collectionPath()
upgrader = self
class UpgradePage(QWizardPage):
def isComplete(self):
return False
def initializePage(self):
self.setTitle(_("Upgrading"))
self.label = l = QLabel()
l.setTextInteractionFlags(Qt.TextSelectableByMouse)
l.setWordWrap(True)
v = QVBoxLayout()
v.addWidget(l)
prog = QProgressBar()
prog.setMaximum(0)
v.addWidget(prog)
l2 = QLabel(_("Please be patient; this can take a while."))
l2.setTextInteractionFlags(Qt.TextSelectableByMouse)
l2.setWordWrap(True)
v.addWidget(l2)
self.setLayout(v)
# run the upgrade in a different thread
self.thread = UpgradeThread(decks, colpath)
self.thread.start()
# and periodically update the GUI
self.timer = QTimer(self)
self.timer.connect(self.timer, SIGNAL("timeout()"), self.onTimer)
self.timer.start(1000)
self.onTimer()
def onTimer(self):
prog = self.thread.progress()
if not prog:
self.timer.stop()
upgrader.log = self.thread.log
upgrader.wizard.next()
self.label.setText(prog)
return UpgradePage()
def _finishedPage(self):
upgrader = self
class FinishedPage(QWizardPage):
def initializePage(self):
buf = ""
for file in upgrader.log:
buf += "<b>%s</b>" % file[0]
buf += "<ul><li>" + "<li>".join(file[1]) + "</ul><p>"
self.setTitle(_("Upgrade Complete"))
l = QLabel(_("""\
The upgrade has finished, and you're ready to start using Anki 2.0.
<p>
Below is a log of the update:
<p>
%s<br><br>""") % buf)
l.setTextFormat(Qt.RichText)
l.setTextInteractionFlags(Qt.TextSelectableByMouse)
l.setWordWrap(True)
l.setMaximumWidth(400)
a = QScrollArea()
a.setWidget(l)
v = QVBoxLayout()
v.addWidget(a)
self.setLayout(v)
return FinishedPage()
class UpgradeThread(QThread):
def __init__(self, paths, colpath):
QThread.__init__(self)
self.paths = paths
self.max = len(paths)
self.current = 1
self.finished = False
self.colpath = colpath
self.name = ""
self.log = []
def run(self):
# open profile deck
self.col = Collection(self.colpath)
# loop through paths
while True:
path = self.paths.pop()
self.name = os.path.basename(path)
self.upgrade(path)
# abort if finished
if not self.paths:
break
self.current += 1
self.col.close()
self.finished = True
def progress(self):
if self.finished:
return
return _("Upgrading deck %(a)s of %(b)s...\n%(c)s") % \
dict(a=self.current, b=self.max, c=self.name)
def upgrade(self, path):
log = self._upgrade(path)
self.log.append((self.name, log))
def _upgrade(self, path):
if not os.path.exists(path):
return [_("File was missing.")]
imp = Anki1Importer(self.col, path)
try:
imp.run()
except Exception, e:
if unicode(e) == "invalidFile":
# already logged
pass
self.col.save()
return imp.log

74
designer/setlang.ui Normal file
View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Anki</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Interface language:</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="lang"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>