2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2016-08-02 03:51:44 +02:00
|
|
|
import io
|
2017-08-26 07:14:20 +02:00
|
|
|
import json
|
|
|
|
import re
|
2015-01-05 02:47:05 +01:00
|
|
|
import zipfile
|
2017-08-28 12:51:43 +02:00
|
|
|
import markdown
|
2017-08-26 07:14:20 +02:00
|
|
|
from send2trash import send2trash
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.qt import *
|
2012-12-22 01:17:10 +01:00
|
|
|
from aqt.utils import showInfo, openFolder, isWin, openLink, \
|
2019-02-18 07:17:14 +01:00
|
|
|
askUser, restoreGeom, saveGeom, showWarning, tooltip, getFile
|
2012-12-21 08:51:59 +01:00
|
|
|
from zipfile import ZipFile
|
|
|
|
import aqt.forms
|
|
|
|
import aqt
|
|
|
|
from aqt.downloader import download
|
2015-01-05 02:47:05 +01:00
|
|
|
from anki.lang import _
|
2017-08-26 07:14:20 +02:00
|
|
|
from anki.utils import intTime
|
|
|
|
from anki.sync import AnkiRequestsClient
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class AddonManager:
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def __init__(self, mw):
|
|
|
|
self.mw = mw
|
2017-08-26 07:14:20 +02:00
|
|
|
self.dirty = False
|
2016-05-31 10:51:40 +02:00
|
|
|
f = self.mw.form
|
2017-08-26 07:14:20 +02:00
|
|
|
f.actionAdd_ons.triggered.connect(self.onAddonsDialog)
|
2012-12-21 08:51:59 +01:00
|
|
|
sys.path.insert(0, self.addonsFolder())
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
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)
|
2018-02-24 03:23:15 +01:00
|
|
|
l.sort()
|
|
|
|
if os.getenv("ANKIREVADDONS", ""):
|
|
|
|
l = reversed(l)
|
2017-08-26 07:14:20 +02:00
|
|
|
return l
|
|
|
|
|
|
|
|
def managedAddons(self):
|
|
|
|
return [d for d in self.allAddons()
|
2017-12-11 08:25:51 +01:00
|
|
|
if re.match(r"^\d+$", d)]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def addonsFolder(self, dir=None):
|
|
|
|
root = self.mw.pm.addonFolder()
|
|
|
|
if not dir:
|
|
|
|
return root
|
|
|
|
return os.path.join(root, dir)
|
2015-09-27 00:55:15 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def loadAddons(self):
|
2017-08-26 07:14:20 +02:00
|
|
|
for dir in self.allAddons():
|
|
|
|
meta = self.addonMeta(dir)
|
|
|
|
if meta.get("disabled"):
|
|
|
|
continue
|
|
|
|
self.dirty = True
|
2015-09-27 00:55:15 +02:00
|
|
|
try:
|
2017-08-26 07:14:20 +02:00
|
|
|
__import__(dir)
|
2015-09-27 00:55:15 +02:00
|
|
|
except:
|
2017-08-26 07:14:20 +02:00
|
|
|
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.
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
When loading '%(name)s':
|
|
|
|
%(traceback)s
|
|
|
|
""") % dict(name=meta.get("name", dir), traceback=traceback.format_exc()))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def onAddonsDialog(self):
|
|
|
|
AddonsDialog(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
# Metadata
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def _addonMetaPath(self, dir):
|
|
|
|
return os.path.join(self.addonsFolder(dir), "meta.json")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def addonMeta(self, dir):
|
|
|
|
path = self._addonMetaPath(dir)
|
|
|
|
try:
|
2017-12-11 08:25:51 +01:00
|
|
|
with open(path, encoding="utf8") as f:
|
|
|
|
return json.load(f)
|
2017-08-26 07:14:20 +02:00
|
|
|
except:
|
|
|
|
return dict()
|
|
|
|
|
|
|
|
def writeAddonMeta(self, dir, meta):
|
|
|
|
path = self._addonMetaPath(dir)
|
2017-12-11 08:25:51 +01:00
|
|
|
with open(path, "w", encoding="utf8") as f:
|
2018-12-15 00:13:10 +01:00
|
|
|
json.dump(meta, f)
|
2017-08-26 07:14:20 +02:00
|
|
|
|
|
|
|
def toggleEnabled(self, dir):
|
|
|
|
meta = self.addonMeta(dir)
|
|
|
|
meta['disabled'] = not meta.get("disabled")
|
|
|
|
self.writeAddonMeta(dir, meta)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def addonName(self, dir):
|
|
|
|
return self.addonMeta(dir).get("name", dir)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
# Installing and deleting add-ons
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2019-02-18 07:17:14 +01:00
|
|
|
def installDownload(self, sid, data, name):
|
2015-01-05 02:47:05 +01:00
|
|
|
try:
|
2019-02-18 07:17:14 +01:00
|
|
|
zfile = ZipFile(io.BytesIO(data))
|
2016-02-18 09:49:44 +01:00
|
|
|
except zipfile.BadZipfile:
|
2015-01-05 02:47:05 +01:00
|
|
|
showWarning(_("The download was corrupt. Please try again."))
|
2019-02-18 07:17:14 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
mod = intTime()
|
|
|
|
|
|
|
|
self.install(sid, zfile, name, mod)
|
2017-08-26 07:14:20 +02:00
|
|
|
|
2019-02-18 07:17:14 +01:00
|
|
|
def installLocal(self, path):
|
|
|
|
try:
|
|
|
|
zfile = ZipFile(path)
|
|
|
|
except zipfile.BadZipfile:
|
|
|
|
return False, _("Corrupt add-on archive.")
|
|
|
|
|
|
|
|
try:
|
|
|
|
with zfile.open("manifest.json") as f:
|
|
|
|
info = json.loads(f.read())
|
|
|
|
package = info["package"]
|
|
|
|
name = info["name"]
|
|
|
|
mod = info.get("mod", None)
|
|
|
|
assert isinstance(package, str) and isinstance(name, str)
|
|
|
|
assert isinstance(mod, int) or mod is None
|
|
|
|
except (KeyError, json.decoder.JSONDecodeError, AssertionError):
|
|
|
|
# raised for missing manifest, invalid json, missing/invalid keys
|
|
|
|
return False, _("Invalid add-on manifest.")
|
|
|
|
|
|
|
|
self.install(package, zfile, name, mod)
|
|
|
|
|
|
|
|
return name, None
|
|
|
|
|
|
|
|
def install(self, dir, zfile, name, mod):
|
2017-08-28 12:51:43 +02:00
|
|
|
# previously installed?
|
2019-02-18 07:17:14 +01:00
|
|
|
meta = self.addonMeta(dir)
|
|
|
|
base = self.addonsFolder(dir)
|
2017-08-26 07:14:20 +02:00
|
|
|
if os.path.exists(base):
|
2019-02-18 07:17:14 +01:00
|
|
|
self.backupUserFiles(dir)
|
|
|
|
self.deleteAddon(dir)
|
2017-08-26 07:14:20 +02:00
|
|
|
|
|
|
|
os.mkdir(base)
|
2019-02-18 07:17:14 +01:00
|
|
|
self.restoreUserFiles(dir)
|
2017-09-10 10:53:47 +02:00
|
|
|
|
|
|
|
# extract
|
2019-02-18 07:17:14 +01:00
|
|
|
for n in zfile.namelist():
|
2012-12-21 08:51:59 +01:00
|
|
|
if n.endswith("/"):
|
|
|
|
# folder; ignore
|
|
|
|
continue
|
2017-09-10 10:53:47 +02:00
|
|
|
|
|
|
|
path = os.path.join(base, n)
|
|
|
|
# skip existing user files
|
|
|
|
if os.path.exists(path) and n.startswith("user_files/"):
|
|
|
|
continue
|
2019-02-18 07:17:14 +01:00
|
|
|
zfile.extract(n, base)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
# update metadata
|
|
|
|
meta['name'] = name
|
2019-02-18 07:17:14 +01:00
|
|
|
if mod is not None: # allow packages to skip updating mod
|
|
|
|
meta['mod'] = mod
|
|
|
|
self.writeAddonMeta(dir, meta)
|
2017-08-26 07:14:20 +02:00
|
|
|
|
|
|
|
def deleteAddon(self, dir):
|
|
|
|
send2trash(self.addonsFolder(dir))
|
|
|
|
|
2019-02-18 07:17:14 +01:00
|
|
|
# Processing local add-on files
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def processAPKX(self, paths):
|
|
|
|
log = []
|
|
|
|
errs = []
|
|
|
|
self.mw.progress.start(immediate=True)
|
|
|
|
for path in paths:
|
|
|
|
base = os.path.basename(path)
|
|
|
|
name, error = self.installLocal(path)
|
|
|
|
if error:
|
|
|
|
errs.append(_("Error installing <i>%(base)s</i>: %(error)s"
|
|
|
|
% dict(base=base, error=error)))
|
|
|
|
else:
|
|
|
|
log.append(_("Installed %(name)s" % dict(name=name)))
|
|
|
|
self.mw.progress.finish()
|
|
|
|
return log, errs
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
# 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
|
2017-09-10 09:02:32 +02:00
|
|
|
fname = fname.replace("_", " ")
|
2017-08-26 07:14:20 +02:00
|
|
|
name = os.path.splitext(fname)[0]
|
2019-02-18 07:17:14 +01:00
|
|
|
self.installDownload(str(n), data, name)
|
2017-08-26 07:14:20 +02:00
|
|
|
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
|
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
# Add-on Config
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
_configButtonActions = {}
|
2018-07-28 09:00:49 +02:00
|
|
|
_configUpdatedActions = {}
|
2017-08-28 12:51:43 +02:00
|
|
|
|
|
|
|
def addonConfigDefaults(self, dir):
|
|
|
|
path = os.path.join(self.addonsFolder(dir), "config.json")
|
|
|
|
try:
|
2017-12-11 08:25:51 +01:00
|
|
|
with open(path, encoding="utf8") as f:
|
|
|
|
return json.load(f)
|
2017-08-28 12:51:43 +02:00
|
|
|
except:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def addonConfigHelp(self, dir):
|
|
|
|
path = os.path.join(self.addonsFolder(dir), "config.md")
|
|
|
|
if os.path.exists(path):
|
2018-03-01 03:54:54 +01:00
|
|
|
with open(path, encoding="utf-8") as f:
|
2017-12-11 08:25:51 +01:00
|
|
|
return markdown.markdown(f.read())
|
2017-08-28 12:51:43 +02:00
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
2017-08-31 06:41:00 +02:00
|
|
|
def addonFromModule(self, module):
|
|
|
|
return module.split(".")[0]
|
|
|
|
|
|
|
|
def configAction(self, addon):
|
|
|
|
return self._configButtonActions.get(addon)
|
|
|
|
|
2018-07-28 09:00:49 +02:00
|
|
|
def configUpdatedAction(self, addon):
|
|
|
|
return self._configUpdatedActions.get(addon)
|
|
|
|
|
2017-08-31 06:41:00 +02:00
|
|
|
# Add-on Config API
|
|
|
|
######################################################################
|
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
def getConfig(self, module):
|
2017-08-31 06:41:00 +02:00
|
|
|
addon = self.addonFromModule(module)
|
2017-08-30 07:31:03 +02:00
|
|
|
# get default config
|
|
|
|
config = self.addonConfigDefaults(addon)
|
|
|
|
if config is None:
|
|
|
|
return None
|
|
|
|
# merge in user's keys
|
2017-08-28 12:51:43 +02:00
|
|
|
meta = self.addonMeta(addon)
|
2017-08-30 07:31:03 +02:00
|
|
|
userConf = meta.get("config", {})
|
|
|
|
config.update(userConf)
|
|
|
|
return config
|
2017-08-28 12:51:43 +02:00
|
|
|
|
2017-08-31 06:41:00 +02:00
|
|
|
def setConfigAction(self, module, fn):
|
|
|
|
addon = self.addonFromModule(module)
|
2017-08-28 12:51:43 +02:00
|
|
|
self._configButtonActions[addon] = fn
|
|
|
|
|
2018-07-28 09:00:49 +02:00
|
|
|
def setConfigUpdatedAction(self, module, fn):
|
|
|
|
addon = self.addonFromModule(module)
|
|
|
|
self._configUpdatedActions[addon] = fn
|
|
|
|
|
2017-08-31 06:41:00 +02:00
|
|
|
def writeConfig(self, module, conf):
|
|
|
|
addon = self.addonFromModule(module)
|
2017-08-28 12:51:43 +02:00
|
|
|
meta = self.addonMeta(addon)
|
|
|
|
meta['config'] = conf
|
|
|
|
self.writeAddonMeta(addon, meta)
|
|
|
|
|
2017-09-10 10:53:47 +02:00
|
|
|
# user_files
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def _userFilesPath(self, sid):
|
|
|
|
return os.path.join(self.addonsFolder(sid), "user_files")
|
|
|
|
|
|
|
|
def _userFilesBackupPath(self):
|
|
|
|
return os.path.join(self.addonsFolder(), "files_backup")
|
|
|
|
|
|
|
|
def backupUserFiles(self, sid):
|
|
|
|
p = self._userFilesPath(sid)
|
|
|
|
if os.path.exists(p):
|
|
|
|
os.rename(p, self._userFilesBackupPath())
|
|
|
|
|
|
|
|
def restoreUserFiles(self, sid):
|
|
|
|
p = self._userFilesPath(sid)
|
|
|
|
bp = self._userFilesBackupPath()
|
|
|
|
# did we back up userFiles?
|
|
|
|
if not os.path.exists(bp):
|
|
|
|
return
|
|
|
|
os.rename(bp, p)
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
# 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)
|
2019-02-18 07:17:14 +01:00
|
|
|
f.installFromFile.clicked.connect(self.onInstallFiles)
|
2017-08-26 07:14:20 +02:00
|
|
|
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)
|
2017-08-28 12:51:43 +02:00
|
|
|
f.config.clicked.connect(self.onConfig)
|
2018-08-31 08:56:16 +02:00
|
|
|
self.form.addonList.currentRowChanged.connect(self._onAddonItemSelected)
|
2019-02-18 07:17:53 +01:00
|
|
|
self.setAcceptDrops(True)
|
2017-08-26 07:14:20 +02:00
|
|
|
self.redrawAddons()
|
|
|
|
self.show()
|
|
|
|
|
2019-02-18 07:17:53 +01:00
|
|
|
def dragEnterEvent(self, event):
|
|
|
|
mime = event.mimeData()
|
|
|
|
if not mime.hasUrls():
|
|
|
|
return None
|
|
|
|
urls = mime.urls()
|
|
|
|
if all(url.toLocalFile().endswith(".apkx") for url in urls):
|
|
|
|
event.acceptProposedAction()
|
|
|
|
|
|
|
|
def dropEvent(self, event):
|
|
|
|
mime = event.mimeData()
|
|
|
|
paths = []
|
|
|
|
for url in mime.urls():
|
|
|
|
path = url.toLocalFile()
|
|
|
|
if os.path.exists(path):
|
|
|
|
paths.append(path)
|
|
|
|
self.onInstallFiles(paths)
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
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)
|
|
|
|
|
2019-02-06 00:19:20 +01:00
|
|
|
self.form.addonList.repaint()
|
|
|
|
|
2018-08-31 08:56:16 +02:00
|
|
|
def _onAddonItemSelected(self, row_int):
|
|
|
|
try:
|
|
|
|
addon = self.addons[row_int][1]
|
2018-08-31 09:13:06 +02:00
|
|
|
except IndexError:
|
2018-08-31 08:56:16 +02:00
|
|
|
addon = ''
|
2018-08-31 09:13:06 +02:00
|
|
|
self.form.viewPage.setEnabled(bool (re.match(r"^\d+$", addon)))
|
2018-08-31 08:56:16 +02:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
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:
|
2018-09-24 06:48:08 +02:00
|
|
|
showInfo(_("Please select a single add-on first."))
|
2017-08-26 07:14:20 +02:00
|
|
|
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
|
2017-12-11 08:25:51 +01:00
|
|
|
if re.match(r"^\d+$", addon):
|
2018-01-16 07:07:30 +01:00
|
|
|
openLink(aqt.appShared + "info/{}".format(addon))
|
2017-08-26 07:14:20 +02:00
|
|
|
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
|
2017-11-19 10:28:04 +01:00
|
|
|
if not askUser(ngettext("Delete the %(num)d selected add-on?",
|
|
|
|
"Delete the %(num)d selected add-ons?",
|
|
|
|
len(selected)) %
|
2017-08-26 07:14:20 +02:00
|
|
|
dict(num=len(selected))):
|
|
|
|
return
|
|
|
|
for dir in selected:
|
|
|
|
self.mgr.deleteAddon(dir)
|
|
|
|
self.redrawAddons()
|
|
|
|
|
|
|
|
def onGetAddons(self):
|
|
|
|
GetAddons(self)
|
|
|
|
|
2019-02-18 07:17:14 +01:00
|
|
|
def onInstallFiles(self, paths=None):
|
|
|
|
if not paths:
|
|
|
|
key = (_("Packaged Anki Add-on") + " (*.apkx)")
|
|
|
|
paths = getFile(self, _("Install Add-on(s)"), None, key,
|
|
|
|
key="addons", multi=True)
|
|
|
|
if not paths:
|
|
|
|
return False
|
|
|
|
|
|
|
|
log, errs = self.mgr.processAPKX(paths)
|
|
|
|
|
|
|
|
if log:
|
|
|
|
tooltip("<br>".join(log), parent=self)
|
|
|
|
if errs:
|
|
|
|
msg = _("Please report this to the respective add-on author(s).")
|
|
|
|
showWarning("<br><br>".join(errs + [msg]), parent=self)
|
|
|
|
|
|
|
|
self.redrawAddons()
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
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()
|
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
def onConfig(self):
|
|
|
|
addon = self.onlyOneSelected()
|
|
|
|
if not addon:
|
|
|
|
return
|
|
|
|
|
|
|
|
# does add-on manage its own config?
|
|
|
|
act = self.mgr.configAction(addon)
|
|
|
|
if act:
|
|
|
|
act()
|
|
|
|
return
|
|
|
|
|
|
|
|
conf = self.mgr.getConfig(addon)
|
|
|
|
if conf is None:
|
|
|
|
showInfo(_("Add-on has no configuration."))
|
|
|
|
return
|
|
|
|
|
|
|
|
ConfigEditor(self, addon, conf)
|
|
|
|
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
# Fetching Add-ons
|
|
|
|
######################################################################
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class GetAddons(QDialog):
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
def __init__(self, dlg):
|
|
|
|
QDialog.__init__(self, dlg)
|
|
|
|
self.addonsDlg = dlg
|
|
|
|
self.mgr = dlg.mgr
|
|
|
|
self.mw = self.mgr.mw
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form = aqt.forms.getaddons.Ui_Dialog()
|
|
|
|
self.form.setupUi(self)
|
|
|
|
b = self.form.buttonBox.addButton(
|
2018-06-21 07:23:41 +02:00
|
|
|
_("Browse Add-ons"), QDialogButtonBox.ActionRole)
|
2016-05-31 10:51:40 +02:00
|
|
|
b.clicked.connect(self.onBrowse)
|
2014-06-18 20:47:45 +02:00
|
|
|
restoreGeom(self, "getaddons", adjustSize=True)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.exec_()
|
2014-06-18 20:47:45 +02:00
|
|
|
saveGeom(self, "getaddons")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onBrowse(self):
|
2017-08-26 14:40:11 +02:00
|
|
|
openLink(aqt.appShared + "addons/2.1")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def accept(self):
|
2017-02-15 06:55:20 +01:00
|
|
|
# get codes
|
|
|
|
try:
|
|
|
|
ids = [int(n) for n in self.form.code.text().split()]
|
|
|
|
except ValueError:
|
|
|
|
showWarning(_("Invalid code."))
|
|
|
|
return
|
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
log, errs = self.mgr.downloadIds(ids)
|
2017-02-15 06:55:20 +01:00
|
|
|
|
2017-08-26 07:14:20 +02:00
|
|
|
if log:
|
|
|
|
tooltip("\n".join(log), parent=self.addonsDlg)
|
|
|
|
if errs:
|
|
|
|
showWarning("\n".join(errs))
|
|
|
|
|
|
|
|
self.addonsDlg.redrawAddons()
|
|
|
|
QDialog.accept(self)
|
2017-08-28 12:51:43 +02:00
|
|
|
|
|
|
|
# Editing config
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
class ConfigEditor(QDialog):
|
|
|
|
|
|
|
|
def __init__(self, dlg, addon, conf):
|
|
|
|
super().__init__(dlg)
|
|
|
|
self.addon = addon
|
|
|
|
self.conf = conf
|
|
|
|
self.mgr = dlg.mgr
|
|
|
|
self.form = aqt.forms.addonconf.Ui_Dialog()
|
|
|
|
self.form.setupUi(self)
|
|
|
|
restore = self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults)
|
|
|
|
restore.clicked.connect(self.onRestoreDefaults)
|
2019-02-15 14:15:54 +01:00
|
|
|
self.setupFonts()
|
2017-08-28 12:51:43 +02:00
|
|
|
self.updateHelp()
|
2018-07-28 09:09:17 +02:00
|
|
|
self.updateText(self.conf)
|
2017-08-28 12:51:43 +02:00
|
|
|
self.show()
|
|
|
|
|
|
|
|
def onRestoreDefaults(self):
|
2018-07-28 09:09:17 +02:00
|
|
|
default_conf = self.mgr.addonConfigDefaults(self.addon)
|
|
|
|
self.updateText(default_conf)
|
2017-08-28 12:51:43 +02:00
|
|
|
|
2019-02-15 14:15:54 +01:00
|
|
|
def setupFonts(self):
|
|
|
|
font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
|
|
|
self.form.editor.setFont(font_mono)
|
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
def updateHelp(self):
|
|
|
|
txt = self.mgr.addonConfigHelp(self.addon)
|
|
|
|
if txt:
|
|
|
|
self.form.label.setText(txt)
|
|
|
|
else:
|
|
|
|
self.form.scrollArea.setVisible(False)
|
|
|
|
|
2018-07-28 09:09:17 +02:00
|
|
|
def updateText(self, conf):
|
2017-08-28 12:51:43 +02:00
|
|
|
self.form.editor.setPlainText(
|
2018-12-15 00:13:10 +01:00
|
|
|
json.dumps(conf, ensure_ascii=False, sort_keys=True,
|
|
|
|
indent=4, separators=(',', ': ')))
|
2017-08-28 12:51:43 +02:00
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
txt = self.form.editor.toPlainText()
|
|
|
|
try:
|
2018-07-28 09:09:17 +02:00
|
|
|
new_conf = json.loads(txt)
|
2017-08-28 12:51:43 +02:00
|
|
|
except Exception as e:
|
|
|
|
showInfo(_("Invalid configuration: ") + repr(e))
|
|
|
|
return
|
|
|
|
|
2018-11-18 06:22:31 +01:00
|
|
|
if not isinstance(new_conf, dict):
|
|
|
|
showInfo(_("Invalid configuration: top level object must be a map"))
|
|
|
|
return
|
|
|
|
|
2018-07-28 09:09:17 +02:00
|
|
|
if new_conf != self.conf:
|
|
|
|
self.mgr.writeConfig(self.addon, new_conf)
|
2018-07-28 09:25:38 +02:00
|
|
|
# does the add-on define an action to be fired?
|
2018-07-28 09:09:17 +02:00
|
|
|
act = self.mgr.configUpdatedAction(self.addon)
|
|
|
|
if act:
|
2018-07-28 09:25:38 +02:00
|
|
|
act(new_conf)
|
2018-07-28 09:00:49 +02:00
|
|
|
|
2017-08-28 12:51:43 +02:00
|
|
|
super().accept()
|