mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
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:
parent
00991e8e8e
commit
e3b7096db5
3 changed files with 96 additions and 20 deletions
|
@ -261,7 +261,7 @@ def parseArgs(argv):
|
||||||
if isMac and len(argv) > 1 and argv[1].startswith("-psn"):
|
if isMac and len(argv) > 1 and argv[1].startswith("-psn"):
|
||||||
argv = [argv[0]]
|
argv = [argv[0]]
|
||||||
parser = argparse.ArgumentParser(description="Anki " + appVersion)
|
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("-b", "--base", help="path to base folder", default="")
|
||||||
parser.add_argument("-p", "--profile", help="profile name to load", default="")
|
parser.add_argument("-p", "--profile", help="profile name to load", default="")
|
||||||
parser.add_argument("-l", "--lang", help="interface language (en, de, etc)")
|
parser.add_argument("-l", "--lang", help="interface language (en, de, etc)")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import defaultdict
|
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
|
from zipfile import ZipFile
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
@ -209,7 +209,9 @@ and have been disabled: %(found)s"
|
||||||
return {}
|
return {}
|
||||||
return manifest
|
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
|
"""Install add-on from path or file-like object. Metadata is read
|
||||||
from the manifest file, with keys overriden by supplying a 'manifest'
|
from the manifest file, with keys overriden by supplying a 'manifest'
|
||||||
dictionary"""
|
dictionary"""
|
||||||
|
@ -284,11 +286,14 @@ and have been disabled: %(found)s"
|
||||||
# Processing local add-on files
|
# 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 = []
|
log = []
|
||||||
errs = []
|
errs = []
|
||||||
|
|
||||||
self.mw.progress.start(immediate=True)
|
self.mw.progress.start(immediate=True, parent=parent)
|
||||||
try:
|
try:
|
||||||
for path in paths:
|
for path in paths:
|
||||||
base = os.path.basename(path)
|
base = os.path.basename(path)
|
||||||
|
@ -661,7 +666,7 @@ class AddonsDialog(QDialog):
|
||||||
def onGetAddons(self):
|
def onGetAddons(self):
|
||||||
GetAddons(self)
|
GetAddons(self)
|
||||||
|
|
||||||
def onInstallFiles(self, paths=None):
|
def onInstallFiles(self, paths: List[str] = None, external: bool = False):
|
||||||
if not paths:
|
if not paths:
|
||||||
key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext)
|
key = _("Packaged Anki Add-on") + " (*{})".format(self.mgr.ext)
|
||||||
paths = getFile(
|
paths = getFile(
|
||||||
|
@ -670,17 +675,7 @@ class AddonsDialog(QDialog):
|
||||||
if not paths:
|
if not paths:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
log, errs = self.mgr.processPackages(paths)
|
installAddonPackages(self.mgr, paths, parent=self)
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
self.redrawAddons()
|
self.redrawAddons()
|
||||||
|
|
||||||
|
@ -855,3 +850,65 @@ class ConfigEditor(QDialog):
|
||||||
|
|
||||||
self.onClose()
|
self.onClose()
|
||||||
super().accept()
|
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
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
@ -1021,6 +1022,13 @@ QTreeWidget {
|
||||||
|
|
||||||
aqt.exporting.ExportDialog(self, did=did)
|
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
|
# Cramming
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -1473,6 +1481,8 @@ will be lost. Continue?"""
|
||||||
self.app.appMsg.connect(self.onAppMsg)
|
self.app.appMsg.connect(self.onAppMsg)
|
||||||
|
|
||||||
def onAppMsg(self, buf: str) -> Optional[QTimer]:
|
def onAppMsg(self, buf: str) -> Optional[QTimer]:
|
||||||
|
is_addon = buf.endswith(".ankiaddon")
|
||||||
|
|
||||||
if self.state == "startup":
|
if self.state == "startup":
|
||||||
# try again in a second
|
# try again in a second
|
||||||
return self.progress.timer(
|
return self.progress.timer(
|
||||||
|
@ -1483,7 +1493,11 @@ will be lost. Continue?"""
|
||||||
if buf == "raise":
|
if buf == "raise":
|
||||||
return None
|
return None
|
||||||
self.pendingImport = buf
|
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():
|
if not self.interactiveState() or self.progress.busy():
|
||||||
# we can't raise the main window while in profile dialog, syncing, etc
|
# we can't raise the main window while in profile dialog, syncing, etc
|
||||||
if buf != "raise":
|
if buf != "raise":
|
||||||
|
@ -1507,8 +1521,13 @@ Please ensure a profile is open and Anki is not busy, then try again."""
|
||||||
self.raise_()
|
self.raise_()
|
||||||
if buf == "raise":
|
if buf == "raise":
|
||||||
return None
|
return None
|
||||||
# import
|
|
||||||
self.handleImport(buf)
|
# import / add-on installation
|
||||||
|
if is_addon:
|
||||||
|
self.installAddon(buf)
|
||||||
|
else:
|
||||||
|
self.handleImport(buf)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# GC
|
# GC
|
||||||
|
|
Loading…
Reference in a new issue