new add-on handling
- separate dialog for managing add-ons - only add-ons compatible with Anki 2.1 will be shown on AnkiWeb - can delete or toggle disabled on multiple add-ons at once - check for updates button - button to view add-on's AnkiWeb page The new handling drops support for single file .py add-ons, and requires add-ons to store all files in a single folder. This ensures all files are cleaned up properly when updating or deleting an add-on, and prevents file conflicts between separate add-ons. See the updated add-on docs for more: https://apps.ankiweb.net/docs/addons21.html#add-on-folders https://apps.ankiweb.net/docs/addons21.html#sharing-add-ons README.addons has been moved to the above page
This commit is contained in:
parent
f824db2143
commit
7288a9b063
@ -1,98 +0,0 @@
|
||||
Porting add-ons to Anki 2.1
|
||||
---------------------------
|
||||
|
||||
Python 3
|
||||
---------
|
||||
|
||||
Anki 2.1 requires Python 3.5 or later. After installing Python 3 on your
|
||||
machine, you can use the 2to3 tool to automatically convert your existing
|
||||
scripts to Python 3 code on a folder by folder basis, like:
|
||||
|
||||
2to3-3.5 --output-dir=aqt3 -W -n aqt
|
||||
mv aqt aqt-old
|
||||
mv aqt3 aqt
|
||||
|
||||
Most simple code can be converted automatically, but there may be parts of the
|
||||
code that you need to manually modify.
|
||||
|
||||
Qt5 / PyQt5
|
||||
------------
|
||||
|
||||
The syntax for connecting signals and slots has changed in PyQt5. Recent PyQt4
|
||||
versions support the new syntax as well, so after updating your add-ons you
|
||||
may find they still work in Anki 2.0.x as well.
|
||||
|
||||
More info is available at
|
||||
http://pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html
|
||||
|
||||
One add-on author reported that the following tool was useful to automatically
|
||||
convert the code:
|
||||
https://github.com/rferrazz/pyqt4topyqt5
|
||||
|
||||
Compatibility with Anki 2.0
|
||||
----------------------------
|
||||
|
||||
It should be possible for many add-ons to support both Anki 2.0 and 2.1 at the
|
||||
same time.
|
||||
|
||||
Most Python 3 code will run on Python 2 as well, though extra work may be
|
||||
required when dealing with file access and byte strings.
|
||||
|
||||
The Qt modules are in 'PyQt5' instead of 'PyQt4'. You can do a conditional
|
||||
import, but an easier way is to import from aqt.qt - eg "from aqt.qt import *"
|
||||
|
||||
The most difficult part is the change from the unsupported QtWebKit to
|
||||
QtWebEngine. If you do any non-trivial work with webviews, some work will be
|
||||
required to port your code to Anki 2.1, as described in the next section.
|
||||
|
||||
If you find that non-trivial changes are required to get your add-on working
|
||||
with Anki 2.1, the easiest option is to drop support for older Anki versions.
|
||||
Please see the 'sharing updated add-ons' section below for more information.
|
||||
|
||||
Webview changes
|
||||
----------------
|
||||
|
||||
Qt 5 has dropped WebKit in favour of the Chromium-based WebEngine, so
|
||||
Anki's webviews are now using WebEngine. Of note:
|
||||
|
||||
- You can now debug the webviews using an external Chrome instance, by setting
|
||||
the env var QTWEBENGINE_REMOTE_DEBUGGING to 8080 prior to starting Anki,
|
||||
then surfing to localhost:8080 in Chrome.
|
||||
- WebEngine uses a different method of communicating back to Python.
|
||||
AnkiWebView() is a wrapper for webviews which provides a pycmd(str) function in
|
||||
Javascript which will call the ankiwebview's onBridgeCmd(str) method. Various
|
||||
parts of Anki's UI like reviewer.py and deckbrowser.py have had to be
|
||||
modified to use this.
|
||||
- Javascript is evaluated asynchronously, so if you need the result of a JS
|
||||
expression you can use ankiwebview's evalWithCallback().
|
||||
- As a result of this asynchronous behaviour, editor.saveNow() now requires a
|
||||
callback. If your add-on performs actions in the browser, you likely need to
|
||||
call editor.saveNow() first and then run the rest of your code in the callback.
|
||||
Calls to .onSearch() will need to be changed to .search()/.onSearchActivated()
|
||||
as well. See the browser's .deleteNotes() for an example.
|
||||
- Various operations that were supported by WebKit like setScrollPosition() now
|
||||
need to be implemented in javascript.
|
||||
- Page actions like mw.web.triggerPageAction(QWebEnginePage.Copy) are also
|
||||
asynchronous, and need to be rewritten to use javascript or a delay.
|
||||
- WebEngine doesn't provide a keyPressEvent() like WebKit did, so the code
|
||||
that catches shortcuts not attached to a menu or button has had to be changed.
|
||||
See the way reviewer.py calls setStateShortcuts() for an example.
|
||||
|
||||
Add-ons without a top level file
|
||||
---------------------------------
|
||||
|
||||
Add-ons no longer require a top level file - if you just distribute a single
|
||||
folder, the folder's __init__.py file will form the entry point. This will not
|
||||
work in 2.0.x however.
|
||||
|
||||
Sharing updated add-ons
|
||||
------------------------
|
||||
|
||||
If you've succeeded in making an add-on that supports both 2.0.x and 2.1.x at
|
||||
the same time, please feel free to upload it to the shared add-ons area, and
|
||||
mention in the description that it works with both versions.
|
||||
|
||||
If you've decided to make a separate 2.1.x version, it's probably best to just
|
||||
post a link to it in your current add-on description or upload it separately.
|
||||
When we get closer to a release I'll look into adding separate uploads for the
|
||||
two versions.
|
@ -5,9 +5,9 @@ For info on contributing things other than code, such as translations, decks
|
||||
and add-ons, please see http://ankisrs.net/docs/manual.html#contributing
|
||||
|
||||
The goal of Anki 2.1.x is to bring Anki up to date with Python 3 and Qt 5,
|
||||
while changing as little else as possible. Modern Linux distros have started
|
||||
dropping support for Qt 4, so we need to keep changes to a minimum in order to
|
||||
get an update out faster.
|
||||
while maintaining compatibility with Anki 2.0.x. Some users will be stuck on
|
||||
Anki 2.0 for a while due to unported add-ons or old hardware, so it's
|
||||
important that 2.1 doesn't make breaking changes to the file format.
|
||||
|
||||
Also of consideration is that the Anki code is indirectly used by the mobile
|
||||
clients, which try their best to keep as close to the Anki code as possible so
|
||||
|
391
aqt/addons.py
391
aqt/addons.py
@ -2,9 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import io
|
||||
import sys, os, traceback
|
||||
from io import StringIO
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from send2trash import send2trash
|
||||
|
||||
from aqt.qt import *
|
||||
from aqt.utils import showInfo, openFolder, isWin, openLink, \
|
||||
askUser, restoreGeom, saveGeom, showWarning, tooltip
|
||||
@ -13,131 +15,103 @@ import aqt.forms
|
||||
import aqt
|
||||
from aqt.downloader import download
|
||||
from anki.lang import _
|
||||
|
||||
# in the future, it would be nice to save the addon id and unzippped file list
|
||||
# to the config so that we can clear up all files and check for updates
|
||||
from anki.utils import intTime
|
||||
from anki.sync import AnkiRequestsClient
|
||||
|
||||
class AddonManager:
|
||||
|
||||
def __init__(self, mw):
|
||||
self.mw = mw
|
||||
self.dirty = False
|
||||
f = self.mw.form
|
||||
f.actionOpenPluginFolder.triggered.connect(self.onOpenAddonFolder)
|
||||
f.actionDownloadSharedPlugin.triggered.connect(self.onGetAddons)
|
||||
self._menus = []
|
||||
if isWin:
|
||||
self.clearAddonCache()
|
||||
f.actionAdd_ons.triggered.connect(self.onAddonsDialog)
|
||||
sys.path.insert(0, self.addonsFolder())
|
||||
if not self.mw.safeMode:
|
||||
self.loadAddons()
|
||||
|
||||
def files(self):
|
||||
return [f for f in os.listdir(self.addonsFolder())
|
||||
if f.endswith(".py")]
|
||||
def allAddons(self):
|
||||
l = []
|
||||
for d in os.listdir(self.addonsFolder()):
|
||||
path = self.addonsFolder(d)
|
||||
if not os.path.exists(os.path.join(path, "__init__.py")):
|
||||
continue
|
||||
l.append(d)
|
||||
return l
|
||||
|
||||
def directories(self):
|
||||
return [d for d in os.listdir(self.addonsFolder())
|
||||
if not d.startswith('.') and
|
||||
not d == "__pycache__" and
|
||||
os.path.isdir(os.path.join(self.addonsFolder(), d))]
|
||||
def managedAddons(self):
|
||||
return [d for d in self.allAddons()
|
||||
if re.match("^\d+$", d)]
|
||||
|
||||
def addonsFolder(self, dir=None):
|
||||
root = self.mw.pm.addonFolder()
|
||||
if not dir:
|
||||
return root
|
||||
return os.path.join(root, dir)
|
||||
|
||||
def loadAddons(self):
|
||||
for file in self.files():
|
||||
try:
|
||||
__import__(file.replace(".py", ""))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
for directory in self.directories():
|
||||
try:
|
||||
__import__(directory)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
self.rebuildAddonsMenu()
|
||||
|
||||
# Menus
|
||||
######################################################################
|
||||
|
||||
def onOpenAddonFolder(self, checked, path=None):
|
||||
if path is None:
|
||||
path = self.addonsFolder()
|
||||
openFolder(path)
|
||||
|
||||
def rebuildAddonsMenu(self):
|
||||
for m in self._menus:
|
||||
self.mw.form.menuPlugins.removeAction(m.menuAction())
|
||||
for file in self.files():
|
||||
m = self.mw.form.menuPlugins.addMenu(
|
||||
os.path.splitext(file)[0])
|
||||
self._menus.append(m)
|
||||
p = os.path.join(self.addonsFolder(), file)
|
||||
a = QAction(_("Edit..."), self.mw, triggered=lambda x, y=p: self.onEdit(y))
|
||||
m.addAction(a)
|
||||
a = QAction(_("Delete..."), self.mw, triggered=lambda x, y=p: self.onRem(y))
|
||||
m.addAction(a)
|
||||
|
||||
def onEdit(self, path):
|
||||
d = QDialog(self.mw)
|
||||
frm = aqt.forms.editaddon.Ui_Dialog()
|
||||
frm.setupUi(d)
|
||||
d.setWindowTitle(os.path.basename(path))
|
||||
frm.text.setPlainText(open(path).read())
|
||||
frm.buttonBox.accepted.connect(lambda: self.onAcceptEdit(path, frm))
|
||||
d.exec_()
|
||||
|
||||
def onAcceptEdit(self, path, frm):
|
||||
open(path, "wb").write(frm.text.toPlainText().encode("utf8"))
|
||||
showInfo(_("Edits saved. Please restart Anki."))
|
||||
|
||||
def onRem(self, path):
|
||||
if not askUser(_("Delete %s?") % os.path.basename(path)):
|
||||
return
|
||||
os.unlink(path)
|
||||
self.rebuildAddonsMenu()
|
||||
showInfo(_("Deleted. Please restart Anki."))
|
||||
|
||||
# Tools
|
||||
######################################################################
|
||||
|
||||
def addonsFolder(self):
|
||||
dir = self.mw.pm.addonFolder()
|
||||
return dir
|
||||
|
||||
def clearAddonCache(self):
|
||||
"Clear .pyc files which may cause crashes if Python version updated."
|
||||
dir = self.addonsFolder()
|
||||
for curdir, dirs, files in os.walk(dir):
|
||||
for f in files:
|
||||
if not f.endswith(".pyc"):
|
||||
for dir in self.allAddons():
|
||||
meta = self.addonMeta(dir)
|
||||
if meta.get("disabled"):
|
||||
continue
|
||||
os.unlink(os.path.join(curdir, f))
|
||||
self.dirty = True
|
||||
try:
|
||||
__import__(dir)
|
||||
except:
|
||||
showWarning(_("""\
|
||||
An add-on you installed failed to load. If problems persist, please \
|
||||
go to the Tools>Add-ons menu, and disable or delete the add-on.
|
||||
|
||||
def registerAddon(self, name, updateId):
|
||||
# not currently used
|
||||
return
|
||||
When loading '%(name)s':
|
||||
%(traceback)s
|
||||
""") % dict(name=meta.get("name", dir), traceback=traceback.format_exc()))
|
||||
|
||||
# Installing add-ons
|
||||
def onAddonsDialog(self):
|
||||
AddonsDialog(self)
|
||||
|
||||
# Metadata
|
||||
######################################################################
|
||||
|
||||
def onGetAddons(self):
|
||||
showInfo("""\
|
||||
Most add-ons built for Anki 2.0.x will not work on this version of Anki \
|
||||
until they are updated. To avoid errors during startup, please only \
|
||||
download add-ons that say they support Anki 2.1.x in the description.""")
|
||||
GetAddons(self.mw)
|
||||
def _addonMetaPath(self, dir):
|
||||
return os.path.join(self.addonsFolder(dir), "meta.json")
|
||||
|
||||
def install(self, data, fname):
|
||||
if fname.endswith(".py"):
|
||||
# .py files go directly into the addon folder
|
||||
path = os.path.join(self.addonsFolder(), fname)
|
||||
open(path, "wb").write(data)
|
||||
return
|
||||
# .zip file
|
||||
def addonMeta(self, dir):
|
||||
path = self._addonMetaPath(dir)
|
||||
try:
|
||||
return json.load(open(path, encoding="utf8"))
|
||||
except:
|
||||
return dict()
|
||||
|
||||
def writeAddonMeta(self, dir, meta):
|
||||
path = self._addonMetaPath(dir)
|
||||
json.dump(meta, open(path, "w", encoding="utf8"))
|
||||
|
||||
def toggleEnabled(self, dir):
|
||||
meta = self.addonMeta(dir)
|
||||
meta['disabled'] = not meta.get("disabled")
|
||||
self.writeAddonMeta(dir, meta)
|
||||
|
||||
def addonName(self, dir):
|
||||
return self.addonMeta(dir).get("name", dir)
|
||||
|
||||
# Installing and deleting add-ons
|
||||
######################################################################
|
||||
|
||||
def install(self, sid, data, fname):
|
||||
try:
|
||||
z = ZipFile(io.BytesIO(data))
|
||||
except zipfile.BadZipfile:
|
||||
showWarning(_("The download was corrupt. Please try again."))
|
||||
return
|
||||
base = self.addonsFolder()
|
||||
|
||||
name = os.path.splitext(fname)[0]
|
||||
|
||||
# remove old version first
|
||||
base = self.addonsFolder(sid)
|
||||
if os.path.exists(base):
|
||||
self.deleteAddon(sid)
|
||||
|
||||
# extract
|
||||
os.mkdir(base)
|
||||
for n in z.namelist():
|
||||
if n.endswith("/"):
|
||||
# folder; ignore
|
||||
@ -145,11 +119,190 @@ download add-ons that say they support Anki 2.1.x in the description.""")
|
||||
# write
|
||||
z.extract(n, base)
|
||||
|
||||
# write metadata
|
||||
meta = dict(name=name,
|
||||
mod=intTime())
|
||||
self.writeAddonMeta(sid, meta)
|
||||
|
||||
def deleteAddon(self, dir):
|
||||
send2trash(self.addonsFolder(dir))
|
||||
|
||||
# Downloading
|
||||
######################################################################
|
||||
|
||||
def downloadIds(self, ids):
|
||||
log = []
|
||||
errs = []
|
||||
self.mw.progress.start(immediate=True)
|
||||
for n in ids:
|
||||
ret = download(self.mw, n)
|
||||
if ret[0] == "error":
|
||||
errs.append(_("Error downloading %(id)s: %(error)s") % dict(id=n, error=ret[1]))
|
||||
continue
|
||||
data, fname = ret
|
||||
self.install(str(n), data, fname)
|
||||
name = os.path.splitext(fname)[0]
|
||||
log.append(_("Downloaded %(fname)s" % dict(fname=name)))
|
||||
self.mw.progress.finish()
|
||||
return log, errs
|
||||
|
||||
# Updating
|
||||
######################################################################
|
||||
|
||||
def checkForUpdates(self):
|
||||
client = AnkiRequestsClient()
|
||||
|
||||
# get mod times
|
||||
self.mw.progress.start(immediate=True)
|
||||
try:
|
||||
# ..of enabled items downloaded from ankiweb
|
||||
addons = []
|
||||
for dir in self.managedAddons():
|
||||
meta = self.addonMeta(dir)
|
||||
if not meta.get("disabled"):
|
||||
addons.append(dir)
|
||||
|
||||
mods = []
|
||||
while addons:
|
||||
chunk = addons[:25]
|
||||
del addons[:25]
|
||||
mods.extend(self._getModTimes(client, chunk))
|
||||
return self._updatedIds(mods)
|
||||
finally:
|
||||
self.mw.progress.finish()
|
||||
|
||||
def _getModTimes(self, client, chunk):
|
||||
resp = client.get(
|
||||
aqt.appShared + "updates/" + ",".join(chunk))
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
else:
|
||||
raise Exception("Unexpected response code from AnkiWeb: {}".format(resp.status_code))
|
||||
|
||||
def _updatedIds(self, mods):
|
||||
updated = []
|
||||
for dir, ts in mods:
|
||||
sid = str(dir)
|
||||
if self.addonMeta(sid).get("mod") < ts:
|
||||
updated.append(sid)
|
||||
return updated
|
||||
|
||||
# Add-ons Dialog
|
||||
######################################################################
|
||||
|
||||
class AddonsDialog(QDialog):
|
||||
|
||||
def __init__(self, addonsManager):
|
||||
self.mgr = addonsManager
|
||||
self.mw = addonsManager.mw
|
||||
|
||||
super().__init__(self.mw)
|
||||
|
||||
f = self.form = aqt.forms.addons.Ui_Dialog()
|
||||
f.setupUi(self)
|
||||
f.getAddons.clicked.connect(self.onGetAddons)
|
||||
f.checkForUpdates.clicked.connect(self.onCheckForUpdates)
|
||||
f.toggleEnabled.clicked.connect(self.onToggleEnabled)
|
||||
f.viewPage.clicked.connect(self.onViewPage)
|
||||
f.viewFiles.clicked.connect(self.onViewFiles)
|
||||
f.delete_2.clicked.connect(self.onDelete)
|
||||
self.redrawAddons()
|
||||
self.show()
|
||||
|
||||
def redrawAddons(self):
|
||||
self.addons = [(self.annotatedName(d), d) for d in self.mgr.allAddons()]
|
||||
self.addons.sort()
|
||||
self.form.addonList.clear()
|
||||
self.form.addonList.addItems([r[0] for r in self.addons])
|
||||
if self.addons:
|
||||
self.form.addonList.setCurrentRow(0)
|
||||
|
||||
def annotatedName(self, dir):
|
||||
meta = self.mgr.addonMeta(dir)
|
||||
buf = self.mgr.addonName(dir)
|
||||
if meta.get('disabled'):
|
||||
buf += _(" (disabled)")
|
||||
return buf
|
||||
|
||||
def selectedAddons(self):
|
||||
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
|
||||
return [self.addons[idx][1] for idx in idxs]
|
||||
|
||||
def onlyOneSelected(self):
|
||||
dirs = self.selectedAddons()
|
||||
if len(dirs) != 1:
|
||||
showInfo("Please select a single add-on first.")
|
||||
return
|
||||
return dirs[0]
|
||||
|
||||
def onToggleEnabled(self):
|
||||
for dir in self.selectedAddons():
|
||||
self.mgr.toggleEnabled(dir)
|
||||
self.redrawAddons()
|
||||
|
||||
def onViewPage(self):
|
||||
addon = self.onlyOneSelected()
|
||||
if not addon:
|
||||
return
|
||||
if re.match("^\d+$", addon):
|
||||
openLink(aqt.appShared + f"info/{addon}")
|
||||
else:
|
||||
showWarning(_("Add-on was not downloaded from AnkiWeb."))
|
||||
|
||||
def onViewFiles(self):
|
||||
# if nothing selected, open top level folder
|
||||
selected = self.selectedAddons()
|
||||
if not selected:
|
||||
openFolder(self.mgr.addonsFolder())
|
||||
return
|
||||
|
||||
# otherwise require a single selection
|
||||
addon = self.onlyOneSelected()
|
||||
if not addon:
|
||||
return
|
||||
path = self.mgr.addonsFolder(addon)
|
||||
openFolder(path)
|
||||
|
||||
def onDelete(self):
|
||||
selected = self.selectedAddons()
|
||||
if not selected:
|
||||
return
|
||||
if not askUser(_("Delete the %(num)d selected add-ons?") %
|
||||
dict(num=len(selected))):
|
||||
return
|
||||
for dir in selected:
|
||||
self.mgr.deleteAddon(dir)
|
||||
self.redrawAddons()
|
||||
|
||||
def onGetAddons(self):
|
||||
GetAddons(self)
|
||||
|
||||
def onCheckForUpdates(self):
|
||||
updated = self.mgr.checkForUpdates()
|
||||
if not updated:
|
||||
tooltip(_("No updates available."))
|
||||
else:
|
||||
names = [self.mgr.addonName(d) for d in updated]
|
||||
if askUser(_("Update the following add-ons?") +
|
||||
"\n" + "\n".join(names)):
|
||||
log, errs = self.mgr.downloadIds(updated)
|
||||
if log:
|
||||
tooltip("\n".join(log), parent=self)
|
||||
if errs:
|
||||
showWarning("\n".join(errs), parent=self)
|
||||
|
||||
self.redrawAddons()
|
||||
|
||||
# Fetching Add-ons
|
||||
######################################################################
|
||||
|
||||
class GetAddons(QDialog):
|
||||
|
||||
def __init__(self, mw):
|
||||
QDialog.__init__(self, mw)
|
||||
self.mw = mw
|
||||
def __init__(self, dlg):
|
||||
QDialog.__init__(self, dlg)
|
||||
self.addonsDlg = dlg
|
||||
self.mgr = dlg.mgr
|
||||
self.mw = self.mgr.mw
|
||||
self.form = aqt.forms.getaddons.Ui_Dialog()
|
||||
self.form.setupUi(self)
|
||||
b = self.form.buttonBox.addButton(
|
||||
@ -160,11 +313,9 @@ class GetAddons(QDialog):
|
||||
saveGeom(self, "getaddons")
|
||||
|
||||
def onBrowse(self):
|
||||
openLink(aqt.appShared + "addons/")
|
||||
openLink(aqt.appShared + "addons/?v=2.1")
|
||||
|
||||
def accept(self):
|
||||
QDialog.accept(self)
|
||||
|
||||
# get codes
|
||||
try:
|
||||
ids = [int(n) for n in self.form.code.text().split()]
|
||||
@ -172,18 +323,12 @@ class GetAddons(QDialog):
|
||||
showWarning(_("Invalid code."))
|
||||
return
|
||||
|
||||
errors = []
|
||||
log, errs = self.mgr.downloadIds(ids)
|
||||
|
||||
self.mw.progress.start(immediate=True)
|
||||
for n in ids:
|
||||
ret = download(self.mw, n)
|
||||
if ret[0] == "error":
|
||||
errors.append(_("Error downloading %(id)s: %(error)s") % dict(id=n, error=ret[1]))
|
||||
continue
|
||||
data, fname = ret
|
||||
self.mw.addonManager.install(data, fname)
|
||||
self.mw.progress.finish()
|
||||
if not errors:
|
||||
tooltip(_("Download successful. Please restart Anki."), period=3000)
|
||||
else:
|
||||
showWarning("\n".join(errors))
|
||||
if log:
|
||||
tooltip("\n".join(log), parent=self.addonsDlg)
|
||||
if errs:
|
||||
showWarning("\n".join(errs))
|
||||
|
||||
self.addonsDlg.redrawAddons()
|
||||
QDialog.accept(self)
|
||||
|
@ -10,7 +10,7 @@ from anki.hooks import addHook, remHook
|
||||
import aqt
|
||||
|
||||
def download(mw, code):
|
||||
"Download addon/deck from AnkiWeb. Caller must start & stop progress diag."
|
||||
"Download addon from AnkiWeb. Caller must start & stop progress diag."
|
||||
# create downloading thread
|
||||
thread = Downloader(code)
|
||||
def onRecv():
|
||||
@ -53,11 +53,11 @@ class Downloader(QThread):
|
||||
client = AnkiRequestsClient()
|
||||
try:
|
||||
resp = client.get(
|
||||
aqt.appShared + "download/%d" % self.code)
|
||||
aqt.appShared + "download/%s?v=2.1" % self.code)
|
||||
if resp.status_code == 200:
|
||||
data = client.streamContent(resp)
|
||||
elif resp.status_code in (403,404):
|
||||
self.error = _("Invalid code")
|
||||
self.error = _("Invalid code, or add-on not available for your version of Anki.")
|
||||
return
|
||||
else:
|
||||
self.error = _("Error downloading: %s" % resp.status_code)
|
||||
|
@ -105,16 +105,17 @@ or your deck may have a problem.
|
||||
<p>If that doesn't fix the problem, please copy the following<br>
|
||||
into a bug report:""")
|
||||
pluginText = _("""\
|
||||
An error occurred in an add-on.<br><br>
|
||||
Please start Anki while holding down the shift key, and see if<br>
|
||||
the error goes away.<br><br>
|
||||
If the error goes away, please report the issue on the add-on<br>
|
||||
forum: %s
|
||||
<br><br>If the error occurs even with add-ons disabled, please<br>
|
||||
report the issue on our support site.
|
||||
<p>An error occurred. Please start Anki while holding down the shift \
|
||||
key, which will temporarily disable the add-ons you have installed.</p>
|
||||
|
||||
<p>If the problem occurs even with add-ons disabled, please report the \
|
||||
issue on our support site.</p>
|
||||
|
||||
<p>If the issue only occurs when add-ons are enabled, plesae use the \
|
||||
Tools>Add-ons menu item to disable one add-on and restart Anki, \
|
||||
repeating until you discover the add-on that is causing the problem.</p>
|
||||
""")
|
||||
pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
|
||||
if "addon" in error:
|
||||
if self.mw.addonManager.dirty:
|
||||
txt = pluginText
|
||||
else:
|
||||
txt = stdText
|
||||
|
104
designer/addons.ui
Normal file
104
designer/addons.ui
Normal file
@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>577</width>
|
||||
<height>379</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Add-ons</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Changes will take effect when Anki is restarted.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="addonList">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ContiguousSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="getAddons">
|
||||
<property name="text">
|
||||
<string>Get Add-ons...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="checkForUpdates">
|
||||
<property name="text">
|
||||
<string>Check for Updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="viewPage">
|
||||
<property name="text">
|
||||
<string>View Add-on Page</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="viewFiles">
|
||||
<property name="text">
|
||||
<string>View Files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="toggleEnabled">
|
||||
<property name="text">
|
||||
<string>Toggle Enabled</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="delete_2">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -79,14 +79,6 @@
|
||||
<property name="title">
|
||||
<string>&Tools</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuPlugins">
|
||||
<property name="title">
|
||||
<string>&Add-ons</string>
|
||||
</property>
|
||||
<addaction name="actionDownloadSharedPlugin"/>
|
||||
<addaction name="actionOpenPluginFolder"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<addaction name="actionStudyDeck"/>
|
||||
<addaction name="actionCreateFiltered"/>
|
||||
<addaction name="separator"/>
|
||||
@ -94,7 +86,7 @@
|
||||
<addaction name="actionCheckMediaDatabase"/>
|
||||
<addaction name="actionEmptyCards"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuPlugins"/>
|
||||
<addaction name="actionAdd_ons"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionNoteTypes"/>
|
||||
<addaction name="actionPreferences"/>
|
||||
@ -237,6 +229,11 @@
|
||||
<string>Ctrl+Shift+N</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAdd_ons">
|
||||
<property name="text">
|
||||
<string>Add-ons...</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="icons.qrc"/>
|
||||
|
Loading…
Reference in New Issue
Block a user