diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py
index 83d69e3a3..1400a6b71 100644
--- a/qt/aqt/__init__.py
+++ b/qt/aqt/__init__.py
@@ -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)")
diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py
index 31f74f252..23a42a00d 100644
--- a/qt/aqt/addons.py
+++ b/qt/aqt/addons.py
@@ -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 = "
".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("
".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 = ",
".join(f"{os.path.basename(p)}" for p in paths)
+ q = _(
+ "Important: As add-ons are programs downloaded from the internet, "
+ "they are potentially malicious."
+ "You should only install add-ons you trust.
"
+ "Are you sure you want to proceed with the installation of the "
+ f"following add-on(s)?
{names_str}"
+ )
+ 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 = "
".join(log)
+ if external:
+ log_html += "
" + _(
+ "Please restart Anki to complete the installation."
+ )
+ 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(
+ "
".join(errs + [msg]),
+ parent=parent,
+ textFormat="rich",
+ title=_("Add-on installation error"),
+ )
+
+ return not errs
diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index 2ae1039e7..d759cf34a 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -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
- self.handleImport(buf)
+
+ # import / add-on installation
+ if is_addon:
+ self.installAddon(buf)
+ else:
+ self.handleImport(buf)
+
return None
# GC