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
|
||||
self.app = app
|
||||
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:
|
||||
self.setupUI()
|
||||
self.setupAddons()
|
||||
|
@ -41,7 +53,6 @@ class AnkiQt(QMainWindow):
|
|||
def setupUI(self):
|
||||
self.col = None
|
||||
self.state = None
|
||||
self.setupLang("en") # bootstrap with english; profile will adjust
|
||||
self.setupThreads()
|
||||
self.setupMainWindow()
|
||||
self.setupStyle()
|
||||
|
@ -67,7 +78,7 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
def setupProfile(self):
|
||||
# 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
|
||||
profs = self.pm.profiles()
|
||||
if len(profs) == 1:
|
||||
|
@ -657,7 +668,7 @@ Please choose a new deck name:"""))
|
|||
##########################################################################
|
||||
|
||||
def setupLang(self, force=None):
|
||||
"Set the user interface language."
|
||||
"Set the user interface language for the current profile."
|
||||
import locale, gettext
|
||||
import anki.lang
|
||||
try:
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
# - Saves in sqlite rather than a flat file so the config can't be corrupted
|
||||
|
||||
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.utils import isMac, isWin, intTime, checksum
|
||||
from anki.lang import langs
|
||||
import aqt.forms
|
||||
|
||||
metaConf = dict(
|
||||
ver=0,
|
||||
|
@ -19,6 +21,7 @@ metaConf = dict(
|
|||
lastMsg=-1,
|
||||
suppressUpdate=False,
|
||||
firstRun=True,
|
||||
defaultLang=None,
|
||||
)
|
||||
|
||||
profileConf = dict(
|
||||
|
@ -103,8 +106,10 @@ documentation for information on using a flash drive.""")
|
|||
self.db.commit()
|
||||
|
||||
def create(self, name):
|
||||
prof = profileConf.copy()
|
||||
prof['lang'] = self.meta['defaultLang']
|
||||
self.db.execute("insert into profiles values (?, ?)",
|
||||
name, cPickle.dumps(profileConf))
|
||||
name, cPickle.dumps(prof))
|
||||
self.db.commit()
|
||||
|
||||
def remove(self, name):
|
||||
|
@ -159,6 +164,7 @@ create table if not exists profiles
|
|||
self.meta = metaConf.copy()
|
||||
self.db.execute("insert into profiles values ('_global', ?)",
|
||||
cPickle.dumps(metaConf))
|
||||
self._setDefaultLang()
|
||||
# and save a default user profile for later (commits)
|
||||
self.create("User 1")
|
||||
else:
|
||||
|
@ -169,3 +175,50 @@ create table if not exists profiles
|
|||
|
||||
def _pwhash(self, 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