diff --git a/aqt/addons.py b/aqt/addons.py index d82ad814e..a644c1fa7 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -2,10 +2,17 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import sys, os, re, traceback +import sys, os, re, traceback, time +from cStringIO import StringIO from aqt.qt import * -from aqt.utils import showInfo, showWarning, openFolder, isWin -from anki.hooks import runHook +from aqt.utils import showInfo, showWarning, openFolder, isWin, openLink +from anki.hooks import runHook, addHook, remHook +from aqt.webview import AnkiWebView +from zipfile import ZipFile +import aqt.forms +import aqt +from anki.sync import httpCon +import aqt.sync # monkey-patches httplib2 class AddonManager(object): @@ -15,6 +22,7 @@ class AddonManager(object): self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenAddonFolder) self.mw.connect(f.actionEnableAllPlugins, s, self.onEnableAllAddons) self.mw.connect(f.actionDisableAllPlugins, s, self.onDisableAllAddons) + self.mw.connect(f.actionDownloadSharedPlugin, s, self.onGetAddons) if isWin: self.clearAddonCache() sys.path.insert(0, self.addonsFolder()) @@ -112,3 +120,106 @@ class AddonManager(object): def registerAddon(self, name, updateId): # not currently used return + + # Installing add-ons + ###################################################################### + + def onGetAddons(self): + GetAddons(self.mw) + + def install(self, data, fname): + if fname.endswith(".py"): + # .py files go directly into the addon folder + path = os.path.join(self.addonsFolder(), fname) + open(path, "w").write(data) + return + # .zip file + z = ZipFile(StringIO(data)) + base = self.addonsFolder() + for n in z.namelist(): + if n.endswith("/"): + # folder; ignore + continue + # write + z.extract(n, base) + +class GetAddons(QDialog): + + def __init__(self, mw): + QDialog.__init__(self, mw) + self.mw = mw + self.form = aqt.forms.getaddons.Ui_Dialog() + self.form.setupUi(self) + b = self.form.buttonBox.addButton( + _("Browse"), QDialogButtonBox.ActionRole) + self.connect(b, SIGNAL("clicked()"), self.onBrowse) + self.exec_() + + def onBrowse(self): + openLink(aqt.appShared + "addons/") + + def accept(self): + try: + code = int(self.form.code.text()) + except ValueError: + showWarning(_("Invalid code.")) + return + QDialog.accept(self) + # create downloader thread + self.thread = AddonDownloader(code) + self.connect(self.thread, SIGNAL("recv"), self.onRecv) + self.recvBytes = 0 + self.thread.start() + self.mw.progress.start(immediate=True) + while not self.thread.isFinished(): + self.mw.app.processEvents() + self.thread.wait(100) + if not self.thread.error: + # success + self.mw.addonManager.install(self.thread.data, self.thread.fname) + self.mw.progress.finish() + showInfo(_("Download successful. Please restart Anki.")) + else: + self.mw.progress.finish() + showWarning(_("Download failed: %s") % self.thread.error) + + def onRecv(self, total): + self.mw.progress.update(label="%dKB downloaded" % (total/1024)) + +class AddonDownloader(QThread): + + def __init__(self, code): + QThread.__init__(self) + self.code = code + + def run(self): + # setup progress handler + self.byteUpdate = time.time() + self.recvTotal = 0 + def canPost(): + if (time.time() - self.byteUpdate) > 0.1: + self.byteUpdate = time.time() + return True + def recvEvent(bytes): + self.recvTotal += bytes + if canPost(): + self.emit(SIGNAL("recv"), self.recvTotal) + addHook("httpRecv", recvEvent) + con = httpCon() + try: + resp, cont = con.request( + aqt.appShared + "download/%d" % self.code) + except Exception, e: + self.error = unicode(e) + return + finally: + remHook("httpRecv", recvEvent) + if resp['status'] == '200': + self.error = None + self.fname = re.match("attachment; filename=(.+)", + resp['content-disposition']).group(1) + self.data = cont + elif resp['status'] == '403': + self.error = _("Invalid code.") + else: + self.error = _("Error downloading: %s") % resp['status'] diff --git a/aqt/getshared.py b/aqt/getshared.py deleted file mode 100644 index eae2e1ff0..000000000 --- a/aqt/getshared.py +++ /dev/null @@ -1,259 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: Damien Elmes -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -from aqt.qt import * -import aqt, simplejson, time, cStringIO, zipfile, os, re, gzip -import traceback, urllib2, socket, cgi -from aqt.ui.utils import saveGeom, restoreGeom, showInfo -from anki.utils import fmtTimeSpan - -URL = "http://ankiweb.net/file/" -#URL = "http://localhost:8001/file/" - -R_ID = 0 -R_USERNAME = 1 -R_TITLE = 2 -R_DESCRIPTION = 3 -R_TAGS = 4 -R_VERSION = 5 -R_FACTS = 6 -R_SIZE = 7 -R_COUNT = 8 -R_MODIFIED = 9 -R_FNAME = 10 - -class GetShared(QDialog): - - def __init__(self, parent, type): - QDialog.__init__(self, parent, Qt.Window) - self.parent = parent - aqt.form = ankiqt.forms.getshared.Ui_Dialog() - self.form.setupUi(self) - self.ok = True - self.conErrMsg = _("""\ -Unable to connect to the server.

-Please check your network connection or try again in a few minutes.

-
-Error was:
%s
""") - restoreGeom(self, "getshared") - self.setupTable() - self.onChangeType(type) - if type == 0: - self.setWindowTitle(_("Download Shared Col")) - else: - self.setWindowTitle(_("Download Shared Plugin")) - if self.ok: - self.exec_() - - def setupTable(self): - self.connect( - self.form.table, SIGNAL("currentCellChanged(int,int,int,int)"), - self.onCellChanged) - self.form.table.verticalHeader().setDefaultSectionSize( - self.parent.pm.profile['editLineSize']) - self.connect(self.form.search, SIGNAL("textChanged(QString)"), - self.limit) - - def fetchData(self): - self.parent.setProgressParent(None) - self.parent.startProgress() - self.parent.updateProgress() - try: - socket.setdefaulttimeout(30) - try: - sock = urllib2.urlopen( - URL + "search?t=%d&c=1" % self.type) - data = sock.read() - try: - data = gzip.GzipFile(fileobj=cStringIO.StringIO(data)).read() - except: - # the server is sending gzipped data, but a transparent - # proxy or antivirus software may be decompressing it - # before we get it - pass - self.allList = simplejson.loads(unicode(data)) - except: - showInfo(self.conErrMsg % cgi.escape(unicode( - traceback.format_exc(), "utf-8", "replace"))) - self.close() - self.ok = False - return - finally: - self.parent.finishProgress() - socket.setdefaulttimeout(None) - self.form.search.setFocus() - self.typeChanged() - self.limit() - - def limit(self, txt=""): - if not txt: - self.curList = self.allList - else: - txt = unicode(txt).lower() - self.curList = [ - l for l in self.allList - if (txt in l[R_TITLE].lower() or - txt in l[R_DESCRIPTION].lower() or - txt in l[R_TAGS].lower())] - self.redraw() - - def redraw(self): - self.form.table.setSortingEnabled(False) - self.form.table.setRowCount(len(self.curList)) - self.items = {} - if self.type == 0: - cols = (R_TITLE, R_FACTS, R_COUNT, R_MODIFIED) - else: - cols = (R_TITLE, R_COUNT, R_MODIFIED) - for rc, r in enumerate(self.curList): - for cc, c in enumerate(cols): - if c == R_FACTS or c == R_COUNT: - txt = unicode("%15d" % r[c]) - elif c == R_MODIFIED: - days = int(((time.time() - r[c])/(24*60*60))) - txt = ngettext("%6d day ago", "%6d days ago", days) % days - else: - txt = unicode(r[c]) - item = QTableWidgetItem(txt) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - self.items[item] = r - self.form.table.setItem(rc, cc, item) - self.form.table.setSortingEnabled(True) - if self.type == 0: - self.form.table.sortItems(2, Qt.DescendingOrder) - else: - self.form.table.sortItems(1, Qt.DescendingOrder) - self.form.table.selectRow(0) - self.onCellChanged(None, None, None, None) - - def onCellChanged(self, row, col, x, y): - ci = self.form.table.currentItem() - if not ci: - self.form.bottomLabel.setText(_("Nothing selected.")) - return - r = self.items[ci] - self.curRow = r - self.form.bottomLabel.setText(_("""\ -Title: %(title)s
-Tags: %(tags)s
-Size: %(size)0.2fKB
-Uploader: %(author)s
-Downloads: %(count)s
-Modified: %(mod)s ago
-
%(description)s""") % { - 'title': r[R_TITLE], - 'tags': r[R_TAGS], - 'size': r[R_SIZE] / 1024.0, - 'author': r[R_USERNAME], - 'count': r[R_COUNT], - 'mod': fmtTimeSpan(time.time() - r[R_MODIFIED]), - 'description': r[R_DESCRIPTION].replace("\n", "
"), - }) - self.form.scrollAreaWidgetContents.adjustSize() - self.form.scrollArea.setWidget(self.form.scrollAreaWidgetContents) - - def onChangeType(self, type): - self.type = type - self.fetchData() - - def typeChanged(self): - self.form.table.clear() - if self.type == 0: - self.form.table.setColumnCount(4) - self.form.table.setHorizontalHeaderLabels([ - _("Title"), _("Facts"), _("Downloads"), _("Modified")]) - else: - self.form.table.setColumnCount(3) - self.form.table.setHorizontalHeaderLabels([ - _("Title"), _("Downloads"), _("Modified")]) - self.form.table.horizontalHeader().setResizeMode( - 0, QHeaderView.Stretch) - self.form.table.verticalHeader().hide() - - def accept(self): - if self.type == 0: - if not self.parent.saveAndClose(hideWelcome=True, parent=self): - return QDialog.accept(self) - # fixme: use namedtmp - (fd, tmpname) = tempfile.mkstemp(prefix="anki") - tmpfile = os.fdopen(fd, "w+b") - cnt = 0 - try: - socket.setdefaulttimeout(30) - self.parent.setProgressParent(self) - self.parent.startProgress() - self.parent.updateProgress() - try: - sock = urllib2.urlopen( - URL + "get?id=%d" % - self.curRow[R_ID]) - while 1: - data = sock.read(32768) - if not data: - break - cnt += len(data) - tmpfile.write(data) - self.parent.updateProgress( - label=_("Downloaded %dKB") % (cnt/1024.0)) - except: - showInfo(self.conErrMsg % cgi.escape(unicode( - traceback.format_exc(), "utf-8", "replace"))) - self.close() - return - finally: - socket.setdefaulttimeout(None) - self.parent.setProgressParent(None) - self.parent.finishProgress() - QDialog.accept(self) - # file is fetched - tmpfile.seek(0) - self.handleFile(tmpfile) - QDialog.accept(self) - - def handleFile(self, file): - ext = os.path.splitext(self.curRow[R_FNAME])[1] - if ext == ".zip": - z = zipfile.ZipFile(file) - else: - z = None - tit = self.curRow[R_TITLE] - tit = re.sub("[^][A-Za-z0-9 ()\-]", "", tit) - tit = tit[0:40] - if self.type == 0: - # col - dd = self.parent.pm.profile['documentDir'] - p = os.path.join(dd, tit + ".anki") - if os.path.exists(p): - tit += "%d" % time.time() - for l in z.namelist(): - if l == "shared.anki": - dpath = os.path.join(dd, tit + ".anki") - open(dpath, "wb").write(z.read(l)) - elif l.startswith("shared.media/"): - try: - os.mkdir(os.path.join(dd, tit + ".media")) - except OSError: - pass - open(os.path.join(dd, tit + ".media", - os.path.basename(l)),"wb").write(z.read(l)) - self.parent.loadCol(dpath) - else: - pd = self.parent.pluginsFolder() - if z: - for l in z.infolist(): - try: - os.makedirs(os.path.join( - pd, os.path.dirname(l.filename))) - except OSError: - pass - if l.filename.endswith("/"): - # directory - continue - path = os.path.join(pd, l.filename) - open(path, "wb").write(z.read(l.filename)) - else: - open(os.path.join(pd, tit + ext), "wb").write(file.read()) - showInfo(_("Plugin downloaded. Please restart Anki."), - parent=self) - diff --git a/designer/getaddons.ui b/designer/getaddons.ui new file mode 100644 index 000000000..6ad8ab28c --- /dev/null +++ b/designer/getaddons.ui @@ -0,0 +1,101 @@ + + + Dialog + + + + 0 + 0 + 367 + 204 + + + + Install Add-on + + + + + + To browse add-ons, please click the browse button below.<br><br>When you've found an add-on you like, please paste its code below. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Code: + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/designer/main.ui b/designer/main.ui index 07565ff0e..62ba64443 100644 --- a/designer/main.ui +++ b/designer/main.ui @@ -200,7 +200,7 @@ - Get Add-ons... + Browse && Install... Download a plugin to add new features or change Anki's behaviour