diff --git a/package_manager/__init__.py b/package_manager/__init__.py index 5c9ca5c..1326bd4 100644 --- a/package_manager/__init__.py +++ b/package_manager/__init__.py @@ -69,7 +69,18 @@ class PackageManagerPreferences(AddonPreferences): def draw(self, context): layout = self.layout - layout.operator("wm.update_index", text="Update List", icon='FILE_REFRESH') + + split = layout.split(percentage=1.0/3) + if (len(self.pm_addons) == 0 or networking.download_install_status in + ("Processing response", "Downloading update", + "Processing failed", "Update failed")): + split.label(text=networking.download_install_status or + "Update add-on list.") + else: + split.label(text="Available add-ons:") + split.separator() + split.operator("wm.update_index", text="Update List", icon='FILE_REFRESH') + rows = 1 if len(self.pm_addons) == 0 else 4 layout.template_list("UI_UL_list", "addons_list", self, "pm_addons", self, "pm_addons_index", rows=rows) @@ -93,7 +104,14 @@ class PackageManagerPreferences(AddonPreferences): else: split.label(text="Installed: No") split.separator() - split.separator() + + if (networking.download_install_status not in + ("Processing response", "Downloading update", + "Processing failed", "Update failed")): + split.label(text=networking.download_install_status) + else: + split.separator() + split.operator("wm.addon_download_install", text="Install from Web", icon='URL').addon = addon.module_name diff --git a/package_manager/networking.py b/package_manager/networking.py index 5e5feac..fb1e637 100644 --- a/package_manager/networking.py +++ b/package_manager/networking.py @@ -21,10 +21,12 @@ import json import logging import os import shutil +import asyncio import urllib.request import zipfile from bpy.props import StringProperty +download_install_status = "" log = logging.getLogger('networking') INDEX_DOWNLOAD_URL = ("https://git.blender.org/gitweb/gitweb.cgi/" @@ -34,24 +36,107 @@ class WM_OT_update_index(bpy.types.Operator): """Check for updated list of add-ons available for download""" bl_idname = "wm.update_index" bl_label = "Check for updated list of add-ons" + + _timer = None + _redraw = False + + @classmethod + def poll(self, context): + global download_install_status + return (not download_install_status + or download_install_status == "Install successful" + or "failed" in download_install_status) + + def __init__ (self): + global download_install_status + download_install_status = "" + + self._redraw = False + + self.loop = asyncio.get_event_loop() + self.loop.stop() def execute(self, context): + # Update add-on index + self.loop.stop() + + self.status("Starting") + + self.loop.run_in_executor(None, self.update_index) + + wm = context.window_manager + wm.modal_handler_add(self) + self._timer = wm.event_timer_add(0.0001, context.window) + + return {'RUNNING_MODAL'} + + def modal(self, context, event): + global download_install_status + + if self._redraw: + context.area.tag_redraw() + self._redraw = False + + if "failed" in download_install_status: + self.cancel(context) + return {'CANCELLED'} + + if download_install_status == "": + self.cancel(context) + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def cancel(self, context): + self.loop.stop() + + wm = context.window_manager + wm.event_timer_remove(self._timer) + + def status(self, text): + global download_install_status + + download_install_status = text + self._redraw = True + + def update_index(self): + self.status("Downloading update") + index_file = self.download_index() + + if index_file: + self.status("Processing response") + + if self.parse_json(index_file): + self.status("") + return + + self.status("Processing failed") + return + + self.status("Update failed") + + def download_index(self): # Download the index.json file try: req = urllib.request.urlopen(INDEX_DOWNLOAD_URL) index_file = req.read().decode('utf-8') req.close() except urllib.error.HTTPError as err: - self.report({'ERROR'}, "Error requesting update: %s %s" % (str(err.code), err.reason)) - return {'CANCELLED'} + log.warning("Error requesting update: %s %s", err.code, err.reason) + return False + except urllib.error.URLError as err: + log.warning("Download failed with URLError: %s", err) + return False + return index_file + + def parse_json(self, index_file): # Parse downloaded file try: addon_list = json.loads(index_file) except ValueError as err: - self.report({'ERROR'}, "Error: JSON file could not parse.") - log.warning("ValueError: %s", err) - return {'CANCELLED'} + log.warning("JSON file could not parse. ValueError: %s", err) + return False # Get the add-on preferences prefs = bpy.context.user_preferences.addons.get("package_manager").preferences @@ -78,7 +163,7 @@ class WM_OT_update_index(bpy.types.Operator): self.load_addon_data(addon, name, content) - return {'FINISHED'} + return True def load_addon_data(self, addon, module_name, content): addon.name = content["name"] @@ -124,6 +209,25 @@ class WM_OT_addon_download_install(bpy.types.Operator): bl_label = "Download and install selected add-on" addon = bpy.props.StringProperty() + + _timer = None + _redraw = False + + @classmethod + def poll(self, context): + global download_install_status + return (not download_install_status + or download_install_status == "Install successful" + or "failed" in download_install_status) + + def __init__ (self): + global download_install_status + download_install_status = "" + + self._redraw = False + + self.loop = asyncio.get_event_loop() + self.loop.stop() def execute(self, context): if self.addon is None: @@ -144,11 +248,61 @@ class WM_OT_addon_download_install(bpy.types.Operator): ext = os.path.splitext(download_url)[1] # Download and install the selected add-on - if self.download(self.addon, download_url): - if self.install(self.addon, ext): - return {'FINISHED'} + self.loop.stop() - return {'CANCELLED'} + self.status("Starting") + + self.loop.run_in_executor(None, self.download_and_install, + self.addon, download_url, ext) + + wm = context.window_manager + wm.modal_handler_add(self) + self._timer = wm.event_timer_add(0.0001, context.window) + + return {'RUNNING_MODAL'} + + def modal(self, context, event): + global download_install_status + + if self._redraw: + context.area.tag_redraw() + self._redraw = False + + if "failed" in download_install_status: + self.cancel(context) + return {'CANCELLED'} + + if download_install_status == "Install successful": + self.cancel(context) + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def cancel(self, context): + self.loop.stop() + + wm = context.window_manager + wm.event_timer_remove(self._timer) + + def status(self, text): + global download_install_status + + download_install_status = text + self._redraw = True + + def download_and_install (self, addon, download_url, filetype): + # Perform download; if successful, install + self.status("Downloading") + if self.download(addon, download_url): + self.status("Installing") + if self.install(addon, filetype): + self.status("Install successful") + return + self.status("Install failed") + return + + self.status("Download failed") + self._redraw = True def download(self, addon, download_url): filetype = os.path.splitext(download_url)[1] @@ -165,6 +319,9 @@ class WM_OT_addon_download_install(bpy.types.Operator): except urllib.error.HTTPError as err: log.warning("Download failed with HTTPError: %s %s", str(err.code), err.reason) return False + except urllib.error.URLError as err: + log.warning("Download failed with URLError: %s", err) + return False return True @@ -184,7 +341,8 @@ class WM_OT_addon_download_install(bpy.types.Operator): shutil.rmtree(addon_path) with zipfile.ZipFile(download_path,"r") as zipped_addon: zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) - + else: + return False return True