mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
upgrade wizard and first startup language selection dialog
This commit is contained in:
parent
7c68b58d44
commit
a1a7e7341c
4 changed files with 379 additions and 5 deletions
17
aqt/main.py
17
aqt/main.py
|
@ -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:
|
||||||
|
|
|
@ -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
236
aqt/upgrade.py
Normal 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
74
designer/setlang.ui
Normal 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>
|
Loading…
Reference in a new issue