Extend CLI with the ability to install .ankiaddon packages

Allows Anki to register a mime-type handler for .ankiaddon files

Other small collateral changes:

+ fix positioning issues with some prompts and progress dialog
+ add prompt titles where they were missing
+ add type annotations for AddonManager installation methods
+ explicitly import os in main (used to be imported via aqt.qt)
This commit is contained in:
Glutanimate 2020-01-03 17:57:33 +01:00
parent 00991e8e8e
commit e3b7096db5
3 changed files with 96 additions and 20 deletions

View file

@ -261,7 +261,7 @@ def parseArgs(argv):
if isMac and len(argv) > 1 and argv[1].startswith("-psn"):
argv = [argv[0]]
parser = argparse.ArgumentParser(description="Anki " + appVersion)
parser.usage = "%(prog)s [OPTIONS] [file to import]"
parser.usage = "%(prog)s [OPTIONS] [file to import/add-on to install]"
parser.add_argument("-b", "--base", help="path to base folder", default="")
parser.add_argument("-p", "--profile", help="profile name to load", default="")
parser.add_argument("-l", "--lang", help="interface language (en, de, etc)")

View file

@ -7,7 +7,7 @@ import os
import re
import zipfile
from collections import defaultdict
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
from typing import IO, Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Union
from zipfile import ZipFile
import jsonschema
@ -209,7 +209,9 @@ and have been disabled: %(found)s"
return {}
return manifest
def install(self, file, manifest=None) -> AddonInstallationResult:
def install(
self, file: Union[IO, str], manifest: dict = None
) -> AddonInstallationResult:
"""Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest'
dictionary"""
@ -284,11 +286,14 @@ and have been disabled: %(found)s"
# Processing local add-on files
######################################################################
def processPackages(self, paths) -> Tuple[List[str], List[str]]:
def processPackages(
self, paths: List[str], parent: QWidget = None
) -> Tuple[List[str], List[str]]:
log = []
errs = []
self.mw.progress.start(immediate=True)
self.mw.progress.start(immediate=True, parent=parent)
try:
for path in paths:
base = os.path.basename(path)
@ -661,7 +666,7 @@ class AddonsDialog(QDialog):
def onGetAddons(self):
GetAddons(self)
def onInstallFiles(self, paths=None):
def onInstallFiles(self, paths: List[str] = None, external: bool = False):
if not paths:
key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext)
paths = getFile(
@ -670,17 +675,7 @@ class AddonsDialog(QDialog):
if not paths:
return False
log, errs = self.mgr.processPackages(paths)
if log:
log_html = "<br>".join(log)
if len(log) == 1:
tooltip(log_html, parent=self)
else:
showInfo(log_html, parent=self, textFormat="rich")
if errs:
msg = _("Please report this to the respective add-on author(s).")
showWarning("<br><br>".join(errs + [msg]), parent=self, textFormat="rich")
installAddonPackages(self.mgr, paths, parent=self)
self.redrawAddons()
@ -855,3 +850,65 @@ class ConfigEditor(QDialog):
self.onClose()
super().accept()
# .ankiaddon installation wizard
######################################################################
def installAddonPackages(
addonsManager: AddonManager,
paths: List[str],
parent: QWidget = None,
external: bool = False,
) -> bool:
if external:
names_str = ",<br>".join(f"<b>{os.path.basename(p)}</b>" for p in paths)
q = _(
"<b>Important</b>: As add-ons are programs downloaded from the internet, "
"they are potentially malicious."
"<b>You should only install add-ons you trust.</b><br><br>"
"Are you sure you want to proceed with the installation of the "
f"following add-on(s)?<br><br>{names_str}<i>"
)
if (
not showInfo(
q,
parent=parent,
title=_("Install Anki add-on"),
type="warning",
customBtns=[QMessageBox.No, QMessageBox.Yes],
)
== QMessageBox.Yes
):
tooltip(_("Add-on installation aborted"), parent=parent)
return False
log, errs = addonsManager.processPackages(paths, parent=parent)
if log:
log_html = "<br>".join(log)
if external:
log_html += "<br><br>" + _(
"<b>Please restart Anki to complete the installation.</b>"
)
if len(log) == 1:
tooltip(log_html, parent=parent)
else:
showInfo(
log_html,
parent=parent,
textFormat="rich",
title=_("Installation complete"),
)
if errs:
msg = _("Please report this to the respective add-on author(s).")
showWarning(
"<br><br>".join(errs + [msg]),
parent=parent,
textFormat="rich",
title=_("Add-on installation error"),
)
return not errs

View file

@ -4,6 +4,7 @@
import faulthandler
import gc
import os
import platform
import re
import signal
@ -1021,6 +1022,13 @@ QTreeWidget {
aqt.exporting.ExportDialog(self, did=did)
# Installing add-ons from CLI / mimetype handler
##########################################################################
def installAddon(self, path):
from aqt.addons import installAddonPackages
installAddonPackages(self.addonManager, [path], external=True, parent=self)
# Cramming
##########################################################################
@ -1473,6 +1481,8 @@ will be lost. Continue?"""
self.app.appMsg.connect(self.onAppMsg)
def onAppMsg(self, buf: str) -> Optional[QTimer]:
is_addon = buf.endswith(".ankiaddon")
if self.state == "startup":
# try again in a second
return self.progress.timer(
@ -1483,7 +1493,11 @@ will be lost. Continue?"""
if buf == "raise":
return None
self.pendingImport = buf
return tooltip(_("Deck will be imported when a profile is opened."))
if is_addon:
msg = _("Add-on will be installed when a profile is opened.")
else:
msg = _("Deck will be imported when a profile is opened.")
return tooltip(msg)
if not self.interactiveState() or self.progress.busy():
# we can't raise the main window while in profile dialog, syncing, etc
if buf != "raise":
@ -1507,8 +1521,13 @@ Please ensure a profile is open and Anki is not busy, then try again."""
self.raise_()
if buf == "raise":
return None
# import
# import / add-on installation
if is_addon:
self.installAddon(buf)
else:
self.handleImport(buf)
return None
# GC