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
2019-02-22 21:14:42 +01:00
from collections import defaultdict
2017-08-28 12:51:43 +02:00
import markdown
2017-08-26 07:14:20 +02:00
from send2trash import send2trash
2019-04-24 21:44:11 +02:00
import jsonschema
from jsonschema . exceptions import ValidationError
2017-08-26 07:14:20 +02:00
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-23 09:02:06 +01:00
askUser , restoreGeom , saveGeom , restoreSplitter , saveSplitter , \
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
2019-03-04 02:58:34 +01:00
from anki . lang import _ , ngettext
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
2019-02-22 10:17:56 +01:00
ext = " .ankiaddon "
2019-02-22 17:04:07 +01:00
_manifest_schema = {
2019-04-24 21:44:11 +02:00
" type " : " object " ,
" properties " : {
" package " : { " type " : " string " , " meta " : False } ,
" name " : { " type " : " string " , " meta " : True } ,
" mod " : { " type " : " number " , " meta " : True } ,
" conflicts " : {
" type " : " array " ,
" items " : { " type " : " string " } ,
" meta " : True
}
} ,
" required " : [ " package " , " name " ]
2019-02-22 17:04:07 +01:00
}
2019-02-22 10:17:56 +01:00
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
""" ) % d ict(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
2019-02-22 21:14:42 +01:00
def isEnabled ( self , dir ) :
2017-08-26 07:14:20 +02:00
meta = self . addonMeta ( dir )
2019-02-22 21:14:42 +01:00
return not meta . get ( ' disabled ' )
def toggleEnabled ( self , dir , enable = None ) :
meta = self . addonMeta ( dir )
enabled = enable if enable is not None else meta . get ( " disabled " )
2019-04-16 09:43:02 +02:00
if enabled is True :
conflicting = self . _disableConflicting ( dir )
if conflicting :
addons = " , " . join ( self . addonName ( f ) for f in conflicting )
showInfo (
_ ( " The following add-ons are incompatible with %(name)s \
and have been disabled : % ( found ) s " ) % d ict(name=self.addonName(dir), found=addons),
textFormat = " plain " )
2019-02-22 21:14:42 +01:00
meta [ ' disabled ' ] = not enabled
2017-08-26 07:14:20 +02:00
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
2019-02-24 14:51:19 +01:00
def annotatedName ( self , dir ) :
buf = self . addonName ( dir )
if not self . isEnabled ( dir ) :
buf + = _ ( " (disabled) " )
return buf
2019-02-22 21:14:42 +01:00
# Conflict resolution
######################################################################
def addonConflicts ( self , dir ) :
return self . addonMeta ( dir ) . get ( " conflicts " , [ ] )
def allAddonConflicts ( self ) :
all_conflicts = defaultdict ( list )
for dir in self . allAddons ( ) :
if not self . isEnabled ( dir ) :
continue
conflicts = self . addonConflicts ( dir )
for other_dir in conflicts :
all_conflicts [ other_dir ] . append ( dir )
return all_conflicts
2019-04-16 09:43:02 +02:00
def _disableConflicting ( self , dir , conflicts = None ) :
2019-02-22 21:14:42 +01:00
conflicts = conflicts or self . addonConflicts ( dir )
installed = self . allAddons ( )
found = [ d for d in conflicts if d in installed and self . isEnabled ( d ) ]
found . extend ( self . allAddonConflicts ( ) . get ( dir , [ ] ) )
if not found :
2019-04-16 09:43:02 +02:00
return [ ]
2019-02-22 21:14:42 +01:00
for package in found :
self . toggleEnabled ( package , enable = False )
2019-04-16 09:43:02 +02:00
return found
2019-02-22 21:14:42 +01:00
2017-08-26 07:14:20 +02:00
# Installing and deleting add-ons
2012-12-21 08:51:59 +01:00
######################################################################
2019-04-24 21:44:11 +02:00
def readManifestFile ( self , zfile ) :
2015-01-05 02:47:05 +01:00
try :
2019-02-22 17:04:07 +01:00
with zfile . open ( " manifest.json " ) as f :
data = json . loads ( f . read ( ) )
2019-04-24 21:44:11 +02:00
jsonschema . validate ( data , self . _manifest_schema )
# build new manifest from recognized keys
schema = self . _manifest_schema [ " properties " ]
manifest = { key : data [ key ] for key in data . keys ( ) & schema . keys ( ) }
except ( KeyError , json . decoder . JSONDecodeError , ValidationError ) :
2019-02-22 17:04:07 +01:00
# raised for missing manifest, invalid json, missing/invalid keys
return { }
return manifest
2017-08-26 07:14:20 +02:00
2019-02-22 17:04:07 +01:00
def install ( self , file , manifest = None ) :
""" Install add-on from path or file-like object. Metadata is read
2019-04-16 09:44:00 +02:00
from the manifest file , with keys overriden by supplying a ' manifest '
dictionary """
2019-02-18 07:17:14 +01:00
try :
2019-02-22 17:04:07 +01:00
zfile = ZipFile ( file )
2019-02-18 07:17:14 +01:00
except zipfile . BadZipfile :
2019-02-22 17:04:07 +01:00
return False , " zip "
2019-02-18 07:17:14 +01:00
2019-02-22 17:04:07 +01:00
with zfile :
2019-04-24 21:44:11 +02:00
file_manifest = self . readManifestFile ( zfile )
2019-04-16 09:44:00 +02:00
if manifest :
file_manifest . update ( manifest )
manifest = file_manifest
2019-02-22 17:04:07 +01:00
if not manifest :
return False , " manifest "
package = manifest [ " package " ]
2019-02-22 21:14:42 +01:00
conflicts = manifest . get ( " conflicts " , [ ] )
2019-04-16 09:43:02 +02:00
found_conflicts = self . _disableConflicting ( package ,
conflicts )
2019-02-22 17:04:07 +01:00
meta = self . addonMeta ( package )
self . _install ( package , zfile )
2019-02-18 07:17:14 +01:00
2019-04-24 21:44:11 +02:00
schema = self . _manifest_schema [ " properties " ]
2019-02-22 17:04:07 +01:00
manifest_meta = { k : v for k , v in manifest . items ( )
if k in schema and schema [ k ] [ " meta " ] }
meta . update ( manifest_meta )
self . writeAddonMeta ( package , meta )
2019-02-18 07:17:14 +01:00
2019-04-16 09:43:02 +02:00
return True , meta [ " name " ] , found_conflicts
2019-02-18 07:17:14 +01:00
2019-02-22 17:04:07 +01:00
def _install ( self , dir , zfile ) :
2017-08-28 12:51:43 +02:00
# previously installed?
2019-02-18 07:17:14 +01:00
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 )
2019-04-29 10:43:10 +02:00
if not self . deleteAddon ( dir ) :
self . restoreUserFiles ( dir )
return
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
2019-04-29 10:43:10 +02:00
# true on success
2017-08-26 07:14:20 +02:00
def deleteAddon ( self , dir ) :
2019-04-29 10:43:10 +02:00
try :
send2trash ( self . addonsFolder ( dir ) )
return True
except OSError as e :
showWarning ( _ ( " Unable to update or delete add-on. Please start Anki while holding down the shift key to temporarily disable add-ons, then try again. \n \n Debug info: %s " ) % e ,
textFormat = " plain " )
return False
2017-08-26 07:14:20 +02:00
2019-02-18 07:17:14 +01:00
# Processing local add-on files
######################################################################
2019-02-22 10:17:56 +01:00
def processPackages ( self , paths ) :
2019-02-18 07:17:14 +01:00
log = [ ]
errs = [ ]
self . mw . progress . start ( immediate = True )
2019-02-27 05:08:20 +01:00
try :
for path in paths :
base = os . path . basename ( path )
ret = self . install ( path )
if ret [ 0 ] is False :
2019-04-16 09:43:02 +02:00
if ret [ 1 ] == " zip " :
2019-02-27 05:08:20 +01:00
msg = _ ( " Corrupt add-on file. " )
elif ret [ 1 ] == " manifest " :
msg = _ ( " Invalid add-on manifest. " )
2019-04-16 09:43:02 +02:00
else :
msg = " Unknown error: {} " . format ( ret [ 1 ] )
2019-02-27 05:08:20 +01:00
errs . append ( _ ( " Error installing <i> %(base)s </i>: %(error)s "
% dict ( base = base , error = msg ) ) )
else :
log . append ( _ ( " Installed %(name)s " % dict ( name = ret [ 1 ] ) ) )
2019-04-16 09:43:02 +02:00
if ret [ 2 ] :
log . append ( _ ( " The following conflicting add-ons were disabled: " ) + " " + " " . join ( ret [ 2 ] ) )
2019-02-27 05:08:20 +01:00
finally :
self . mw . progress . finish ( )
2019-02-18 07:17:14 +01:00
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-22 17:04:07 +01:00
ret = self . install ( io . BytesIO ( data ) ,
manifest = { " package " : str ( n ) , " name " : name ,
" mod " : intTime ( ) } )
if ret [ 0 ] is False :
if ret [ 1 ] == " zip " :
2019-04-16 09:43:02 +02:00
msg = _ ( " Corrupt add-on file. " )
2019-02-22 17:04:07 +01:00
elif ret [ 1 ] == " manifest " :
2019-04-16 09:43:02 +02:00
msg = _ ( " Invalid add-on manifest. " )
else :
msg = " Unknown error: {} " . format ( ret [ 1 ] )
errs . append ( _ ( " Error downloading %(id)s : %(error)s " ) % dict (
id = n , error = msg ) )
else :
log . append ( _ ( " Downloaded %(fname)s " % dict ( fname = name ) ) )
if ret [ 2 ] :
log . append ( _ ( " The following conflicting add-ons were disabled: " ) + " " + " " . join ( ret [ 2 ] ) )
2017-08-26 07:14:20 +02:00
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 )
2019-04-08 17:51:15 +02:00
if self . addonMeta ( sid ) . get ( " mod " , 0 ) < ( ts or 0 ) :
2017-08-26 07:14:20 +02:00
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 )
2019-03-02 18:57:51 +01:00
# Web Exports
######################################################################
_webExports = { }
def setWebExports ( self , module , pattern ) :
addon = self . addonFromModule ( module )
self . _webExports [ addon ] = pattern
def getWebExports ( self , addon ) :
return self . _webExports . get ( addon )
2017-09-10 10:53:47 +02:00
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 ( )
2019-02-20 05:38:22 +01:00
restoreGeom ( self , " addons " )
2017-08-26 07:14:20 +02:00
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 ( )
2019-02-22 10:17:56 +01:00
ext = self . mgr . ext
if all ( url . toLocalFile ( ) . endswith ( ext ) for url in urls ) :
2019-02-18 07:17:53 +01:00
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 )
2019-02-20 05:38:22 +01:00
def reject ( self ) :
saveGeom ( self , " addons " )
return QDialog . reject ( self )
2017-08-26 07:14:20 +02:00
def redrawAddons ( self ) :
2019-02-23 10:04:45 +01:00
addonList = self . form . addonList
mgr = self . mgr
2019-02-24 14:51:19 +01:00
self . addons = [ ( mgr . annotatedName ( d ) , d ) for d in mgr . allAddons ( ) ]
2017-08-26 07:14:20 +02:00
self . addons . sort ( )
2019-02-23 10:10:05 +01:00
selected = set ( self . selectedAddons ( ) )
2019-02-23 10:04:45 +01:00
addonList . clear ( )
for name , dir in self . addons :
item = QListWidgetItem ( name , addonList )
if not mgr . isEnabled ( dir ) :
item . setForeground ( Qt . gray )
2019-02-23 10:10:05 +01:00
if dir in selected :
item . setSelected ( True )
2017-08-26 07:14:20 +02:00
2019-02-23 10:04:45 +01:00
addonList . repaint ( )
2019-02-06 00:19:20 +01:00
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 = ' '
2019-04-15 14:53:18 +02:00
self . form . viewPage . setEnabled ( bool ( re . match ( r " ^ \ d+$ " , addon ) ) )
self . form . config . setEnabled ( bool ( self . mgr . getConfig ( addon ) or
self . mgr . configAction ( addon ) ) )
2018-08-31 08:56:16 +02:00
2017-08-26 07:14:20 +02:00
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 :
2019-04-29 10:43:10 +02:00
if not self . mgr . deleteAddon ( dir ) :
break
2019-02-24 06:24:31 +01:00
self . form . addonList . clearSelection ( )
2017-08-26 07:14:20 +02:00
self . redrawAddons ( )
def onGetAddons ( self ) :
GetAddons ( self )
2019-02-18 07:17:14 +01:00
def onInstallFiles ( self , paths = None ) :
if not paths :
2019-02-22 10:17:56 +01:00
key = ( _ ( " Packaged Anki Add-on " ) + " (* {} ) " . format ( self . mgr . ext ) )
2019-02-18 07:17:14 +01:00
paths = getFile ( self , _ ( " Install Add-on(s) " ) , None , key ,
key = " addons " , multi = True )
if not paths :
return False
2019-02-22 10:17:56 +01:00
log , errs = self . mgr . processPackages ( paths )
2019-02-18 07:17:14 +01:00
if log :
2019-04-16 09:38:38 +02:00
log_html = " <br> " . join ( log )
if len ( log ) == 1 :
tooltip ( log_html , parent = self )
else :
showInfo ( log_html , parent = self , textFormat = " rich " )
2019-02-18 07:17:14 +01:00
if errs :
msg = _ ( " Please report this to the respective add-on author(s). " )
2019-04-16 09:39:08 +02:00
showWarning ( " <br><br> " . join ( errs + [ msg ] ) , parent = self , textFormat = " rich " )
2019-02-18 07:17:14 +01:00
self . redrawAddons ( )
2017-08-26 07:14:20 +02:00
def onCheckForUpdates ( self ) :
2019-02-27 05:18:16 +01:00
try :
updated = self . mgr . checkForUpdates ( )
except Exception as e :
showWarning ( _ ( " Please check your internet connection. " ) + " \n \n " + str ( e ) ,
textFormat = " plain " )
return
2017-08-26 07:14:20 +02:00
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 :
2019-04-16 09:38:38 +02:00
log_html = " <br> " . join ( log )
if len ( log ) == 1 :
tooltip ( log_html , parent = self )
else :
showInfo ( log_html , parent = self , textFormat = " rich " )
2017-08-26 07:14:20 +02:00
if errs :
2019-02-26 00:36:02 +01:00
showWarning ( " \n \n " . join ( errs ) , parent = self , textFormat = " plain " )
2017-08-26 07:14:20 +02:00
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 :
2019-04-16 09:38:38 +02:00
log_html = " <br> " . join ( log )
if len ( log ) == 1 :
tooltip ( log_html , parent = self )
else :
showInfo ( log_html , parent = self , textFormat = " rich " )
2017-08-26 07:14:20 +02:00
if errs :
2019-02-26 00:36:02 +01:00
showWarning ( " \n \n " . join ( errs ) , textFormat = " plain " )
2017-08-26 07:14:20 +02:00
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 )
2019-02-23 09:02:06 +01:00
restoreGeom ( self , " addonconf " )
restoreSplitter ( self . form . splitter , " addonconf " )
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 )
2019-02-23 09:12:55 +01:00
tooltip ( _ ( " Restored defaults " ) , parent = self )
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 )
2019-02-23 09:02:06 +01:00
font_mono . setPointSize ( font_mono . pointSize ( ) + 1 )
2019-02-15 14:15:54 +01:00
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
2019-02-23 09:02:06 +01:00
def onClose ( self ) :
saveGeom ( self , " addonconf " )
saveSplitter ( self . form . splitter , " addonconf " )
def reject ( self ) :
self . onClose ( )
super ( ) . reject ( )
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 )
2019-02-23 09:02:06 +01:00
self . onClose ( )
2017-08-28 12:51:43 +02:00
super ( ) . accept ( )