From 2e06cc99450e8eb64d660a9e8c999f4446d64f36 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 21 Oct 2024 22:17:38 +0100 Subject: [PATCH 1/4] Initial Updater - More simple updater compared to cats --- catsold.py | 986 ++++++++++++++++++++++++++++++ core/properties.py | 8 +- core/updater.py | 223 +++++++ resources/translations/en_US.json | 16 +- 4 files changed, 1230 insertions(+), 3 deletions(-) create mode 100644 catsold.py create mode 100644 core/updater.py diff --git a/catsold.py b/catsold.py new file mode 100644 index 0000000..818052b --- /dev/null +++ b/catsold.py @@ -0,0 +1,986 @@ +# MIT License + +import os +import ssl +import bpy +import time +import json +import urllib +import shutil +import pathlib +import zipfile +import addon_utils +from threading import Thread +from collections import OrderedDict +from bpy.app.handlers import persistent +from .tools.translations import t +from .tools.common import wrap_dynamic_enum_items +from . import CATS_VERSION, dev_branch + +no_ver_check = False +fake_update = False + +is_checking_for_update = False +checked_on_startup = False +version_list = None +current_version = [] +current_version_str = '' +update_needed = False +latest_version = None +latest_version_str = '' +used_updater_panel = False +update_finished = False +remind_me_later = False +is_ignored_version = False + +confirm_update_to = '' + +show_error = '' + +main_dir = os.path.dirname(__file__) +downloads_dir = os.path.join(main_dir, "downloads") +resources_dir = os.path.join(main_dir, "resources") +ignore_ver_file = os.path.join(resources_dir, "ignore_version.txt") +no_auto_ver_check_file = os.path.join(resources_dir, "no_auto_ver_check.txt") + +# Get package name, important for panel in user preferences +package_name = '' +for mod in addon_utils.modules(): + if mod.bl_info['name'] == 'Cats Blender Plugin': + package_name = mod.__name__ + +# Icons for UI +ICON_URL = 'URL' + +class CheckForUpdateButton(bpy.types.Operator): + bl_idname = 'cats_updater.check_for_update' + bl_label = t('CheckForUpdateButton.label') + bl_description = t('CheckForUpdateButton.desc') + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + return not is_checking_for_update + + def execute(self, context): + global used_updater_panel + used_updater_panel = True + check_for_update_background() + return {'FINISHED'} + + +class UpdateToLatestButton(bpy.types.Operator): + bl_idname = 'cats_updater.update_latest' + bl_label = t('UpdateToLatestButton.label') + bl_description = t('UpdateToLatestButton.desc') + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + return update_needed + + def execute(self, context): + global confirm_update_to, used_updater_panel + confirm_update_to = 'latest' + used_updater_panel = True + + bpy.ops.cats_updater.confirm_update_panel('INVOKE_DEFAULT') + return {'FINISHED'} + + +class UpdateToSelectedButton(bpy.types.Operator): + bl_idname = 'cats_updater.update_selected' + bl_label = t('UpdateToSelectedButton.label') + bl_description = t('UpdateToSelectedButton.desc') + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + if is_checking_for_update or not version_list: + return False + return True + + def execute(self, context): + global confirm_update_to, used_updater_panel + confirm_update_to = context.scene.cats_updater_version_list + used_updater_panel = True + + bpy.ops.cats_updater.confirm_update_panel('INVOKE_DEFAULT') + return {'FINISHED'} + + +class UpdateToDevButton(bpy.types.Operator): + bl_idname = 'cats_updater.update_dev' + bl_label = t('UpdateToDevButton.label') + bl_description = t('UpdateToDevButton.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + global confirm_update_to, used_updater_panel + confirm_update_to = 'dev' + used_updater_panel = True + + bpy.ops.cats_updater.confirm_update_panel('INVOKE_DEFAULT') + return {'FINISHED'} + + +class RemindMeLaterButton(bpy.types.Operator): + bl_idname = 'cats_updater.remind_me_later' + bl_label = t('RemindMeLaterButton.label') + bl_description = t('RemindMeLaterButton.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + global remind_me_later + remind_me_later = True + self.report({'INFO'}, t('RemindMeLaterButton.success')) + return {'FINISHED'} + + +class IgnoreThisVersionButton(bpy.types.Operator): + bl_idname = 'cats_updater.ignore_this_version' + bl_label = t('IgnoreThisVersionButton.label') + bl_description = t('IgnoreThisVersionButton.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + set_ignored_version() + self.report({'INFO'}, t('IgnoreThisVersionButton.success', name=latest_version_str)) + return {'FINISHED'} + + +class ShowPatchnotesPanel(bpy.types.Operator): + bl_idname = 'cats_updater.show_patchnotes' + bl_label = t('ShowPatchnotesPanel.label') + bl_description = t('ShowPatchnotesPanel.desc') + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + if is_checking_for_update or not version_list: + return False + return True + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + global used_updater_panel + used_updater_panel = True + dpi_value = get_user_preferences().system.dpi + return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 8.2)) + + def check(self, context): + # Important for changing options + return True + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + + row = col.row(align=True) + row.prop(context.scene, 'cats_updater_version_list') + + if context.scene.cats_updater_version_list: + version = version_list.get(context.scene.cats_updater_version_list) + + col.separator() + row = col.row(align=True) + row.label(text=t('ShowPatchnotesPanel.releaseDate', date=version[2])) + + col.separator() + for line in version[1].replace('**', '').split('\r\n'): + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=line) + + col.separator() + + +class ConfirmUpdatePanel(bpy.types.Operator): + bl_idname = 'cats_updater.confirm_update_panel' + bl_label = t('ConfirmUpdatePanel.label') + bl_description = t('ConfirmUpdatePanel.desc') + bl_options = {'INTERNAL'} + + show_patchnotes = False + + def execute(self, context): + print('UPDATE TO ' + confirm_update_to) + if confirm_update_to == 'dev': + update_now(dev=True) + elif confirm_update_to == 'latest': + update_now(latest=True) + else: + update_now(version=confirm_update_to) + return {'FINISHED'} + + def invoke(self, context, event): + dpi_value = get_user_preferences().system.dpi + return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.1)) + + def check(self, context): + # Important for changing options + return True + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + + version_str = confirm_update_to + if confirm_update_to == 'latest': + version_str = latest_version_str + elif confirm_update_to == 'dev': + version_str = 'Dev' + + col.separator() + row = col.row(align=True) + row.label(text='Version: ' + version_str) + + if confirm_update_to == 'dev': + col.separator() + col.separator() + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('ConfirmUpdatePanel.warn.dev1')) + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('ConfirmUpdatePanel.warn.dev2')) + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('ConfirmUpdatePanel.warn.dev3')) + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('ConfirmUpdatePanel.warn.dev4')) + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('ConfirmUpdatePanel.warn.dev5')) + + else: + row.operator(ShowPatchnotesPanel.bl_idname, text=t('ConfirmUpdatePanel.ShowPatchnotesPanel.label')) + + col.separator() + col.separator() + # col.separator() + row = col.row(align=True) + row.scale_y = 0.65 + # row.label(text='Update now to ' + version_str + ':', icon=ICON_URL) + row.label(text=t('ConfirmUpdatePanel.updateNow'), icon=ICON_URL) + + +class UpdateCompletePanel(bpy.types.Operator): + bl_idname = 'cats_updater.update_complete_panel' + bl_label = t('UpdateCompletePanel.label') + bl_description = t('UpdateCompletePanel.desc') + bl_options = {'INTERNAL'} + + show_patchnotes = False + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + dpi_value = get_user_preferences().system.dpi + return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.1)) + + def check(self, context): + # Important for changing options + return True + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + + if update_finished: + row = col.row(align=True) + row.scale_y = 0.9 + row.label(text=t('UpdateCompletePanel.success1'), icon='FILE_TICK') + + row = col.row(align=True) + row.scale_y = 0.9 + row.label(text=t('UpdateCompletePanel.success2'), icon='BLANK1') + else: + row = col.row(align=True) + row.scale_y = 0.9 + row.label(text=t('UpdateCompletePanel.failure1'), icon='CANCEL') + + row = col.row(align=True) + row.scale_y = 0.9 + row.label(text=t('UpdateCompletePanel.failure2'), icon='BLANK1') + + +class UpdateNotificationPopup(bpy.types.Operator): + bl_idname = 'cats_updater.update_notification_popup' + bl_label = t('UpdateNotificationPopup.label') + bl_description = t('UpdateNotificationPopup.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + action = context.scene.cats_update_action + if action == 'UPDATE': + update_now(latest=True) + elif action == 'IGNORE': + set_ignored_version() + else: + # Remind later aka defer + global remind_me_later + remind_me_later = True + ui_refresh() + return {'FINISHED'} + + def invoke(self, context, event): + dpi_value = get_user_preferences().system.dpi + return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.6)) + + # def invoke(self, context, event): + # return context.window_manager.invoke_props_dialog(self) + + def check(self, context): + # Important for changing options + return True + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + + row = layout_split(col, factor=0.55, align=True) + row.scale_y = 1.05 + row.label(text=t('UpdateNotificationPopup.newUpdate', name=latest_version_str), icon='SOLO_ON') + row.operator(ShowPatchnotesPanel.bl_idname, text=t('UpdateNotificationPopup.ShowPatchnotesPanel.label')) + + col.separator() + col.separator() + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'cats_update_action', expand=True) + + +def check_for_update_background(check_on_startup=False): + global is_checking_for_update, checked_on_startup + if check_on_startup and checked_on_startup: + # print('ALREADY CHECKED ON STARTUP') + return + if is_checking_for_update: + # print('ALREADY CHECKING') + return + + checked_on_startup = True + + if check_on_startup and os.path.isfile(no_auto_ver_check_file): + print('AUTO CHECK DISABLED VIA FILE') + return + + is_checking_for_update = True + + thread = Thread(target=check_for_update, args=[]) + thread.start() + + +def check_for_update(): + print('Checking for Cats update...') + + # Get all releases from Github + if not get_github_releases('teamneoneko'): + finish_update_checking(error=t('check_for_update.cantCheck')) + return + + # Check if an update is needed + global update_needed, is_ignored_version + update_needed = check_for_update_available() + is_ignored_version = check_ignored_version() + + # Update needed, show the notification popup if it wasn't checked through the UI + if update_needed: + print('Update found!') + if not used_updater_panel and not is_ignored_version: + prepare_to_show_update_notification() + else: + print('No update found.') + + # Finish update checking, update the UI + finish_update_checking() + + +def get_github_releases(repo): + global version_list + version_list = OrderedDict() + + if fake_update: + print('FAKE INSTALL!') + + version = 'v-99-99-99' + version_tag = version.replace('-', '.') + if version_tag.startswith('v.'): + version_tag = version_tag[2:] + if version_tag.startswith('v'): + version_tag = version_tag[1:] + + version_list[version_tag] = ['', 'Put exiting new stuff here', 'Today'] + version_list['12.34.56.78'] = ['', 'Nothing new to see', 'A week ago probably'] + return True + + try: + ssl._create_default_https_context = ssl._create_unverified_context + with urllib.request.urlopen('https://api.github.com/repos/' + repo + '/Cats-Blender-Plugin-Unofficial-/releases') as url: + data = json.loads(url.read().decode()) + except urllib.error.URLError: + print('URL ERROR') + return False + if not data: + return False + + if bpy.app.version >= (4, 2) and bpy.app.version < (4, 3): + tag_prefix = "4.2." + + for version in data: + full_tag = version.get('tag_name') + if not full_tag.startswith(tag_prefix): + continue + + version_tag = full_tag[len(tag_prefix):] + + # Normalize version_tag + version_tag = version_tag.replace('-', '.') + + # Store full tag + version_list[full_tag] = [ + version['zipball_url'], + version['body'], + version['published_at'].split('T')[0] + ] + + return True + + +def check_for_update_available(): + if not version_list: + return False + + global latest_version, latest_version_str + latest_version = [] + for version in version_list.keys(): + latest_version_str = version + for i in version.split('.'): + if i.isdigit(): + latest_version.append(int(i)) + if latest_version: + break + + # print(latest_version, '>', current_version) + if latest_version > current_version: + return True + + +def finish_update_checking(error=''): + global is_checking_for_update, show_error + is_checking_for_update = False + + # Only show error if the update panel was used before + if used_updater_panel: + show_error = error + + ui_refresh() + + +def ui_refresh(): + # A way to refresh the ui + refreshed = False + while not refreshed: + if hasattr(bpy.data, 'window_managers'): + for windowManager in bpy.data.window_managers: + for window in windowManager.windows: + for area in window.screen.areas: + area.tag_redraw() + refreshed = True + # print('Refreshed UI') + else: + time.sleep(0.5) + + +def get_update_post(): + if hasattr(bpy.app.handlers, 'scene_update_post'): + return bpy.app.handlers.scene_update_post + else: + return bpy.app.handlers.depsgraph_update_post + + +def prepare_to_show_update_notification(): + # This is necessary to show a popup directly after startup + # You will get a nasty error otherwise + # This will add the function to the scene_update_post and it will be executed every frame. that's why it needs to be removed again asap + # print('PREPARE TO SHOW UI') + if show_update_notification not in get_update_post(): + get_update_post().append(show_update_notification) + + +@persistent +def show_update_notification(scene): # One argument in necessary for some reason + # print('SHOWING UI NOW!!!!') + + # # Immediately remove this from handlers again + if show_update_notification in get_update_post(): + get_update_post().remove(show_update_notification) + + # Show notification popup + atr = UpdateNotificationPopup.bl_idname.split(".") + getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') + + +def update_now(version=None, latest=False, dev=False): + if fake_update: + finish_update() + return + if dev: + print('UPDATE TO DEVELOPMENT') + update_link = 'https://github.com/teamneoneko/Cats-Blender-Plugin-Unofficial-/archive/blender-42-dev.zip' + elif latest or not version: + print('UPDATE TO ' + latest_version_str) + update_link = version_list.get(latest_version_str)[0] + bpy.context.scene.cats_updater_version_list = latest_version_str + else: + print('UPDATE TO ' + version) + update_link = version_list[version][0] + + download_file(update_link) + + +def download_file(update_url): + # Load all the directories and files + update_zip_file = os.path.join(downloads_dir, "cats-update.zip") + + # Remove existing download folder + if os.path.isdir(downloads_dir): + print("DOWNLOAD FOLDER EXISTED") + shutil.rmtree(downloads_dir) + + # Create download folder + pathlib.Path(downloads_dir).mkdir(exist_ok=True) + + # Download zip + print('DOWNLOAD FILE') + try: + ssl._create_default_https_context = ssl._create_unverified_context + urllib.request.urlretrieve(update_url, update_zip_file) + except urllib.error.URLError: + print("FILE COULD NOT BE DOWNLOADED") + shutil.rmtree(downloads_dir) + finish_update(error=t('download_file.cantConnect')) + return + print('DOWNLOAD FINISHED') + + # If zip is not downloaded, abort + if not os.path.isfile(update_zip_file): + print("ZIP NOT FOUND!") + shutil.rmtree(downloads_dir) + finish_update(error=t('download_file.cantFindZip')) + return + + # Extract the downloaded zip + print('EXTRACTING ZIP') + with zipfile.ZipFile(update_zip_file, "r") as zip_ref: + zip_ref.extractall(downloads_dir) + print('EXTRACTED') + + # Delete the extracted zip file + print('REMOVING ZIP FILE') + os.remove(update_zip_file) + + # Detect the extracted folders and files + print('SEARCHING FOR INIT 1') + + def searchInit(path): + print('SEARCHING IN ' + path) + files = os.listdir(path) + if "__init__.py" in files: + print('FOUND') + return path + folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] + if len(folders) != 1: + print(len(folders), 'FOLDERS DETECTED') + return None + print('GOING DEEPER') + return searchInit(os.path.join(path, folders[0])) + + print('SEARCHING FOR INIT 2') + extracted_zip_dir = searchInit(downloads_dir) + if not extracted_zip_dir: + print("INIT NOT FOUND!") + shutil.rmtree(downloads_dir) + # finish_reloading() + finish_update(error=t('download_file.cantFindCATS')) + return + + # Remove old addon files + clean_addon_dir() + + # Move the extracted files to their correct places + def move_files(from_dir, to_dir): + print('MOVE FILES TO DIR:', to_dir) + files = os.listdir(from_dir) + for file in files: + file_dir = os.path.join(from_dir, file) + target_dir = os.path.join(to_dir, file) + print('MOVE', file_dir) + + # If file exists + if os.path.isfile(file_dir) and os.path.isfile(target_dir): + os.remove(target_dir) + shutil.move(file_dir, to_dir) + print('REMOVED AND MOVED', file) + + elif os.path.isdir(file_dir) and os.path.isdir(target_dir): + move_files(file_dir, target_dir) + + else: + shutil.move(file_dir, to_dir) + print('MOVED', file) + + move_files(extracted_zip_dir, main_dir) + + # Delete download folder + print('DELETE DOWNLOADS DIR') + shutil.rmtree(downloads_dir) + + # Finish the update + finish_update() + + +def finish_update(error=''): + global update_finished, show_error + show_error = error + + if not error: + update_finished = True + + bpy.ops.cats_updater.update_complete_panel('INVOKE_DEFAULT') + ui_refresh() + print("UPDATE DONE!") + + +def clean_addon_dir(): + print("CLEAN ADDON FOLDER") + + # first remove root files and folders (except update folder, important folders and resource folder) + files = [f for f in os.listdir(main_dir) if os.path.isfile(os.path.join(main_dir, f))] + folders = [f for f in os.listdir(main_dir) if os.path.isdir(os.path.join(main_dir, f))] + + for f in files: + file = os.path.join(main_dir, f) + try: + os.remove(file) + print("Clean removing file {}".format(file)) + except OSError: + print("Failed to pre-remove file " + file) + + for f in folders: + folder = os.path.join(main_dir, f) + if f.startswith('.') or f == 'resources' or f == 'downloads': + continue + + try: + shutil.rmtree(folder) + print("Clean removing folder and contents {}".format(folder)) + except OSError: + print("Failed to pre-remove folder " + folder) + + # then remove resource files and folders (except settings and google dict) + resources_folder = os.path.join(main_dir, 'resources') + files = [f for f in os.listdir(resources_folder) if os.path.isfile(os.path.join(resources_folder, f))] + folders = [f for f in os.listdir(resources_folder) if os.path.isdir(os.path.join(resources_folder, f))] + + for f in files: + if f == 'settings.json' or f == 'dictionary_google.json': + continue + file = os.path.join(resources_folder, f) + try: + os.remove(file) + print("Clean removing file {}".format(file)) + except OSError: + print("Failed to pre-remove " + file) + + for f in folders: + folder = os.path.join(resources_folder, f) + try: + shutil.rmtree(folder) + print("Clean removing folder and contents {}".format(folder)) + except OSError: + print("Failed to pre-remove folder " + folder) + + +def set_ignored_version(): + # Create resources folder + pathlib.Path(resources_dir).mkdir(exist_ok=True) + + # Create ignore file + with open(ignore_ver_file, 'w', encoding="utf8") as outfile: + outfile.write(latest_version_str) + + # Set ignored status + global is_ignored_version + is_ignored_version = True + print('IGNORE VERSION ' + latest_version_str) + + +def check_ignored_version(): + if not os.path.isfile(ignore_ver_file): + # print('IGNORE FILE NOT FOUND') + return False + + # Read ignore file + with open(ignore_ver_file, 'r', encoding="utf8") as outfile: + version = outfile.read() + + # Check if the latest version matches the one in the ignore file + if latest_version_str == version: + print('Update ignored.') + return True + + # Delete ignore version file if the latest version is not the version in the file + try: + os.remove(ignore_ver_file) + except OSError: + print("FAILED TO REMOVE IGNORE VERSION FILE") + + return False + + +def get_version_list(self, context): + choices = [] + if version_list: + for version in version_list.keys(): + choices.append((version, version, version)) + + return choices + + +def get_user_preferences(): + return bpy.context.user_preferences if hasattr(bpy.context, 'user_preferences') else bpy.context.preferences + + +def layout_split(layout, factor=0.0, align=False): + return layout.split(factor=factor, align=align) + + +def draw_update_notification_panel(layout): + if not update_needed or remind_me_later or is_ignored_version: + # pass + return + + col = layout.column(align=True) + + if update_finished: + col.separator() + row = col.row(align=True) + row.label(text=t('draw_update_notification_panel.success'), icon='ERROR') + col.separator() + return + + row = col.row(align=True) + row.scale_y = 0.75 + row.label(text=t('draw_update_notification_panel.newUpdate', name=latest_version_str), icon='SOLO_ON') + + col.separator() + row = col.row(align=True) + row.scale_y = 1.3 + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_update_notification_panel.UpdateToLatestButton.label')) + + row = col.row(align=True) + row.scale_y = 1 + row.operator(RemindMeLaterButton.bl_idname, text=t('draw_update_notification_panel.RemindMeLaterButton.label')) + row.operator(IgnoreThisVersionButton.bl_idname, text=t('draw_update_notification_panel.IgnoreThisVersionButton.label')) + + +def draw_updater_panel(context, layout, user_preferences=False): + col = layout.column(align=True) + + scale_big = 2 + scale_small = 1.2 + + row = col.row(align=True) + row.scale_y = 0.8 + row.label(text=t('draw_updater_panel.updateLabel') if not user_preferences else t('draw_updater_panel.updateLabel_alt'), icon=ICON_URL) + col.separator() + + if update_finished: + col.separator() + row = col.row(align=True) + row.label(text=t('draw_updater_panel.success'), icon='ERROR') + col.separator() + return + + if show_error: + row = col.row(align=True) + row.label(text=show_error, icon='ERROR') + col.separator() + + if is_checking_for_update: + if not used_updater_panel: + row = col.row(align=True) + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label')) + else: + split = col.row(align=True) + row = split.row(align=True) + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label')) + row = split.row(align=True) + row.alignment = 'RIGHT' + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH') + + elif update_needed: + split = col.row(align=True) + row = split.row(align=True) + row.scale_y = scale_big + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_updater_panel.UpdateToLatestButton.label', name=latest_version_str)) + row = split.row(align=True) + row.alignment = 'RIGHT' + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH') + + elif not used_updater_panel or not version_list: + row = col.row(align=True) + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label_alt')) + + else: + split = col.row(align=True) + row = split.row(align=True) + row.scale_y = scale_big + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_updater_panel.UpdateToLatestButton.label_alt')) + row = split.row(align=True) + row.alignment = 'RIGHT' + row.scale_y = scale_big + row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH') + + # col.separator() + # col.separator() + # col.separator() + # row = layout_split(col, factor=0.6, align=True) + # row.scale_y = 0.9 + # row.active = True if not is_checking_for_update and version_list else False + # row.label(text="Select Version:") + # row.prop(context.scene, 'cats_updater_version_list', text='') + # + # row = layout_split(col, factor=0.6, align=True) + # row.scale_y = scale_small + # row.operator(UpdateToSelectedButton.bl_idname, text='Install Selected Version') + # row.operator(ShowPatchnotesPanel.bl_idname, text='Show Patchnotes') + + col.separator() + col.separator() + split = col.row(align=True) + row = layout_split(split, factor=0.55, align=True) + row.scale_y = scale_small + row.active = True if not is_checking_for_update and version_list else False + row.operator(UpdateToSelectedButton.bl_idname, text=t('draw_updater_panel.UpdateToSelectedButton.label')) + row.prop(context.scene, 'cats_updater_version_list', text='') + row = split.row(align=True) + row.scale_y = scale_small + row.operator(ShowPatchnotesPanel.bl_idname, text="", icon='WORDWRAP_ON') + + # topsplit = layout_split(col, factor=0.55, align=True) + # + # split = topsplit.row(align=True) + # row = split.row(align=True) + # row.scale_y = scale_small + # row.active = True if not is_checking_for_update and version_list else False + # row.operator(UpdateToSelectedButton.bl_idname, text='Install Version:') + # + # row = split.row(align=True) + # row.alignment = 'RIGHT' + # row.scale_y = scale_small + # row.operator(ShowPatchnotesPanel.bl_idname, text="", icon='WORDWRAP_ON') + # + # row = topsplit.row(align=True) + # row.scale_y = scale_small + # row.prop(context.scene, 'cats_updater_version_list', text='') + + row = col.row(align=True) + row.scale_y = scale_small + row.operator(UpdateToDevButton.bl_idname, text=t('draw_updater_panel.UpdateToDevButton.label')) + + col.separator() + row = col.row(align=True) + row.scale_y = 0.65 + row.label(text=t('draw_updater_panel.currentVersion', name=current_version_str)) + + +# demo bare-bones preferences +class DemoPreferences(bpy.types.AddonPreferences): + bl_idname = package_name + + def draw(self, context): + layout = self.layout + draw_updater_panel(context, layout, user_preferences=True) + + +to_register = [ + CheckForUpdateButton, + UpdateToLatestButton, + UpdateToSelectedButton, + UpdateToDevButton, + RemindMeLaterButton, + IgnoreThisVersionButton, + ShowPatchnotesPanel, + ConfirmUpdatePanel, + UpdateCompletePanel, + UpdateNotificationPopup, + DemoPreferences, +] + + +def register(dev_branch, version_str): + # print('REGISTER CATS UPDATER') + global current_version, fake_update, current_version_str + + # If not dev branch, always disable fake update! + if not dev_branch: + fake_update = False + current_version_str = version_str + + # Get current version + current_version = [] + version_parts = CATS_VERSION.split(".") + + for part in version_parts: + current_version.append(int(part)) + + bpy.types.Scene.cats_updater_version_list = bpy.props.EnumProperty( + name=t('bpy.types.Scene.cats_updater_version_list.label'), + description=t('bpy.types.Scene.cats_updater_version_list.desc'), + items=wrap_dynamic_enum_items(get_version_list, 'cats_updater_version_list', sort=False) + ) + bpy.types.Scene.cats_update_action = bpy.props.EnumProperty( + name=t('bpy.types.Scene.cats_update_action.label'), + description=t('bpy.types.Scene.cats_update_action.desc'), + items=[ + ("UPDATE", t('bpy.types.Scene.cats_update_action.update.label'), t('bpy.types.Scene.cats_update_action.update.desc')), + ("IGNORE", t('bpy.types.Scene.cats_update_action.ignore.label'), t( 'bpy.types.Scene.cats_update_action.ignore.desc')), + ("DEFER", t('bpy.types.Scene.cats_update_action.defer.label'), t( 'bpy.types.Scene.cats_update_action.defer.desc')) + ] + ) + + # Register all Updater classes + count = 0 + for cls in to_register: + try: + bpy.utils.register_class(cls) + count += 1 + except ValueError: + pass + # print('Registered', count, 'CATS updater classes.') + if count < len(to_register): + print('Skipped', len(to_register) - count, 'CATS updater classes.') + + +def unregister(): + # Unregister all Updater classes + for cls in reversed(to_register): + try: + bpy.utils.unregister_class(cls) + except RuntimeError: + pass + + if hasattr(bpy.types.Scene, 'cats_updater_version_list'): + del bpy.types.Scene.cats_updater_version_list \ No newline at end of file diff --git a/core/properties.py b/core/properties.py index e75d2ae..ce0c140 100644 --- a/core/properties.py +++ b/core/properties.py @@ -5,6 +5,7 @@ from bpy.types import Scene, Object, Material, Context from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from ..core.addon_preferences import get_preference from ..core.common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items, get_armatures_that_are_not_selected +from .updater import get_version_list def register() -> None: default_language = get_preference("language", 0) @@ -74,6 +75,12 @@ def register() -> None: name=t("Quick_Access.selected_armature.label"), description=t("Quick_Access.selected_armature.desc") ))) + + register_property((bpy.types.Scene, "avatar_toolkit_updater_version_list", bpy.props.EnumProperty( + name=t('Scene.avatar_toolkit_updater_version_list.name'), + description=t('Scene.avatar_toolkit_updater_version_list.description'), + items=get_version_list + ))) #happy with how compressed this get_texture_node_list method is - @989onan def get_texture_node_list(self: Material, context: Context) -> list[set[3]]: @@ -131,7 +138,6 @@ def register() -> None: get=MaterialListBool.get_bool, set=MaterialListBool.set_bool))) - def unregister() -> None: #if you register properties with register_property then you shouldn't need this function. pass diff --git a/core/updater.py b/core/updater.py new file mode 100644 index 0000000..08eac2f --- /dev/null +++ b/core/updater.py @@ -0,0 +1,223 @@ +import bpy +import os +import ssl +import json +import urllib +import shutil +import pathlib +import zipfile +from threading import Thread +from bpy.app.handlers import persistent +from ..functions.translations import t +from .addon_preferences import get_preference +from .register import register_wrap +from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME + +GITHUB_REPO = "teamneoneko/Avatar-Toolkit-rinadev" + +is_checking_for_update = False +update_needed = False +latest_version = None +latest_version_str = '' +version_list = None + +main_dir = os.path.dirname(os.path.dirname(__file__)) +downloads_dir = os.path.join(main_dir, "downloads") + +@register_wrap +class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator): + bl_idname = 'avatar_toolkit.check_for_update' + bl_label = t('CheckForUpdateButton.label') + bl_description = t('CheckForUpdateButton.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + check_for_update_background() + return {'FINISHED'} + +@register_wrap +class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator): + bl_idname = 'avatar_toolkit.update_latest' + bl_label = t('UpdateToLatestButton.label') + bl_description = t('UpdateToLatestButton.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + update_now(latest=True) + return {'FINISHED'} + +@register_wrap +class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel): + bl_label = t("Updater.label") + bl_idname = "OBJECT_PT_avatar_toolkit_updater" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = CATEGORY_NAME + bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname + bl_order = 5 + + def draw(self, context): + layout = self.layout + draw_updater_panel(context, layout) + +def check_for_update_background(): + global is_checking_for_update + if is_checking_for_update: + return + + is_checking_for_update = True + Thread(target=check_for_update).start() + +def check_for_update(): + global update_needed, latest_version, latest_version_str, version_list + + if not get_github_releases(): + finish_update_checking(error=t('check_for_update.cantCheck')) + return + + update_needed = check_for_update_available() + + if update_needed: + print('Update found!') + else: + print('No update found.') + + finish_update_checking() + +def get_github_releases(): + global version_list + version_list = {} + + try: + ssl._create_default_https_context = ssl._create_unverified_context + with urllib.request.urlopen(f'https://api.github.com/repos/{GITHUB_REPO}/releases') as url: + data = json.loads(url.read().decode()) + except urllib.error.URLError: + print('URL ERROR') + return False + + for version in data: + version_tag = version['tag_name'] + version_list[version_tag] = [ + version['zipball_url'], + version['body'], + version['published_at'].split('T')[0] + ] + + return True + +def check_for_update_available(): + global latest_version, latest_version_str + if not version_list: + return False + + latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')]) + latest_version_str = latest_version + + current_version = get_preference("version") + current_version_parts = [int(x) for x in current_version.split('.')] + latest_version_parts = [int(x) for x in latest_version.split('.')] + + return latest_version_parts > current_version_parts + +def finish_update_checking(error=''): + global is_checking_for_update + is_checking_for_update = False + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + +def update_now(latest=False): + if latest: + update_link = version_list[latest_version_str][0] + else: + update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0] + + download_file(update_link) + +def download_file(update_url): + update_zip_file = os.path.join(downloads_dir, "avatar-toolkit-update.zip") + + if os.path.isdir(downloads_dir): + shutil.rmtree(downloads_dir) + + pathlib.Path(downloads_dir).mkdir(exist_ok=True) + + try: + ssl._create_default_https_context = ssl._create_unverified_context + urllib.request.urlretrieve(update_url, update_zip_file) + except urllib.error.URLError: + finish_update(error=t('download_file.cantConnect')) + return + + if not os.path.isfile(update_zip_file): + finish_update(error=t('download_file.cantFindZip')) + return + + with zipfile.ZipFile(update_zip_file, "r") as zip_ref: + zip_ref.extractall(downloads_dir) + + os.remove(update_zip_file) + + extracted_zip_dir = find_init_directory(downloads_dir) + if not extracted_zip_dir: + finish_update(error=t('download_file.cantFindAvatarToolkit')) + return + + clean_addon_dir() + move_files(extracted_zip_dir, main_dir) + shutil.rmtree(downloads_dir) + + finish_update() + +def find_init_directory(path): + for root, dirs, files in os.walk(path): + if "__init__.py" in files: + return root + return None + +def clean_addon_dir(): + for item in os.listdir(main_dir): + item_path = os.path.join(main_dir, item) + if item.startswith('.') or item in ['resources', 'downloads']: + continue + if os.path.isfile(item_path): + os.remove(item_path) + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + +def move_files(from_dir, to_dir): + for item in os.listdir(from_dir): + s = os.path.join(from_dir, item) + d = os.path.join(to_dir, item) + if os.path.isdir(s): + shutil.copytree(s, d, dirs_exist_ok=True) + else: + shutil.copy2(s, d) + +def finish_update(error=''): + if error: + print(f"Update failed: {error}") + else: + print("Update successful!") + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + +def get_version_list(self, context): + return [(v, v, '') for v in version_list.keys()] if version_list else [] + +def draw_updater_panel(context, layout): + col = layout.column(align=True) + + if is_checking_for_update: + col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label')) + elif update_needed: + col.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToLatestButton.label', name=latest_version_str)) + else: + col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label_alt')) + + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'avatar_toolkit_updater_version_list', text='') + row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label')) + + col.separator() + col.label(text=t('Updater.currentVersion').format(name=get_preference("version"))) + diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index bd727c8..6c3927c 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -249,7 +249,19 @@ "MMDOptions.renaming_bones": "Renaming bones", "MMDOptions.armature_optimization_complete": "Armature optimization complete", "MMDOptions.convert_materials.label": "Convert Materials", - "MMDOptions.convert_materials.desc": "Convert materials to use Principled BSDF shader and fix MMD and VRM shaders", "MMDOptions.converting_materials": "Converting materials for {name}" - + "MMDOptions.convert_materials.desc": "Convert materials to use Principled BSDF shader and fix MMD and VRM shaders", "MMDOptions.converting_materials": "Converting materials for {name}", + "Updater.label": "Updater", + "Updater.CheckForUpdateButton.label": "Check for Updates", + "Updater.CheckForUpdateButton.label_alt": "No Updates Available", + "Updater.UpdateToLatestButton.label": "Update to {name}", + "Updater.UpdateToSelectedButton.label": "Update", + "Updater.currentVersion": "Current Version: {name}", + "Scene.avatar_toolkit_updater_version_list.name": "Available Versions", + "Scene.avatar_toolkit_updater_version_list.description": "List of available versions to update to", + "check_for_update.cantCheck": "Unable to check for updates", + "download_file.cantConnect": "Cannot connect to update server", + "download_file.cantFindZip": "Update file not found", + "download_file.cantFindAvatarToolkit": "Avatar Toolkit files not found in update package" + } } From 1b908a4200bd263ddb29246310271edf9f457828 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 22 Oct 2024 00:08:07 +0100 Subject: [PATCH 2/4] Fixes --- blender_manifest.toml | 2 +- core/addon_preferences.py | 8 +++ core/updater.py | 100 +++++++++++++++++++++++++++++++++----- functions/translations.py | 4 +- 4 files changed, 100 insertions(+), 14 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index d48b252..3c76c9d 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "4.3.1" +version = "0.1.0" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" diff --git a/core/addon_preferences.py b/core/addon_preferences.py index 7949f1d..c4dea3c 100644 --- a/core/addon_preferences.py +++ b/core/addon_preferences.py @@ -1,5 +1,6 @@ import bpy import os +import toml import json from bpy.types import AddonPreferences from typing import Any, Dict @@ -8,6 +9,13 @@ from typing import Any, Dict PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__)) PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json") +def get_current_version(): + main_dir = os.path.dirname(os.path.dirname(__file__)) + manifest_path = os.path.join(main_dir, "blender_manifest.toml") + with open(manifest_path, 'r') as f: + manifest_data = toml.load(f) + return manifest_data.get('version', 'Unknown') + def save_preference(key: str, value: Any) -> None: """Save a single preference to the JSON file.""" prefs = load_preferences() diff --git a/core/updater.py b/core/updater.py index 08eac2f..61cee3c 100644 --- a/core/updater.py +++ b/core/updater.py @@ -9,11 +9,11 @@ import zipfile from threading import Thread from bpy.app.handlers import persistent from ..functions.translations import t -from .addon_preferences import get_preference +from .addon_preferences import get_preference, get_current_version, save_preference from .register import register_wrap from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME -GITHUB_REPO = "teamneoneko/Avatar-Toolkit-rinadev" +GITHUB_REPO = "teamneoneko/Avatar-Toolkit" is_checking_for_update = False update_needed = False @@ -46,6 +46,26 @@ class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator): update_now(latest=True) return {'FINISHED'} +@register_wrap +class AvatarToolkit_OT_UpdateNotificationPopup(bpy.types.Operator): + bl_idname = "avatar_toolkit.update_notification_popup" + bl_label = t('UpdateNotificationPopup.label') + bl_description = t('UpdateNotificationPopup.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + update_now(latest=True) + self.report({'INFO'}, "Update started. Please wait for the process to complete.") + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.label(text=t('UpdateNotificationPopup.newUpdate', default="New update available: {version}").format(version=latest_version_str)) + @register_wrap class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel): bl_label = t("Updater.label") @@ -54,12 +74,39 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 5 + bl_order = 9 def draw(self, context): layout = self.layout draw_updater_panel(context, layout) +@register_wrap +class AvatarToolkit_OT_RestartBlenderPopup(bpy.types.Operator): + bl_idname = "avatar_toolkit.restart_blender_popup" + bl_label = t('RestartBlenderPopup.label', default="Restart Blender") + bl_description = t('RestartBlenderPopup.desc', default="Restart Blender to complete the update") + bl_options = {'INTERNAL'} + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.label(text=t('RestartBlenderPopup.message', default="Update successful! Please restart Blender.")) + +@persistent +def check_for_update_on_start(dummy): + if get_preference("check_for_updates_on_startup", True): + current_time = time.time() + last_check = get_preference("last_update_check", 0) + if current_time - last_check > 86400: # 24 hours + check_for_update_background() + save_preference("last_update_check", current_time) + def check_for_update_background(): global is_checking_for_update if is_checking_for_update: @@ -72,17 +119,18 @@ def check_for_update(): global update_needed, latest_version, latest_version_str, version_list if not get_github_releases(): - finish_update_checking(error=t('check_for_update.cantCheck')) + bpy.app.timers.register(lambda: finish_update_checking(error=t('check_for_update.cantCheck'))) return update_needed = check_for_update_available() if update_needed: print('Update found!') + bpy.app.timers.register(lambda: bpy.ops.avatar_toolkit.update_notification_popup('INVOKE_DEFAULT') or None) else: print('No update found.') - finish_update_checking() + bpy.app.timers.register(finish_update_checking) def get_github_releases(): global version_list @@ -114,16 +162,31 @@ def check_for_update_available(): latest_version = max(version_list.keys(), key=lambda v: [int(x) for x in v.split('.')]) latest_version_str = latest_version - current_version = get_preference("version") - current_version_parts = [int(x) for x in current_version.split('.')] - latest_version_parts = [int(x) for x in latest_version.split('.')] + current_version = get_current_version() + print(f"Current version: {current_version}") # Debugging statement + + if current_version is None: + print("Current version is None. Cannot check for updates.") + return False + + try: + # Validate that the version string contains only numeric parts + current_version_parts = [int(x) for x in current_version.split('.')] + latest_version_parts = [int(x) for x in latest_version.split('.')] + except ValueError as e: + print(f"Error parsing version numbers: {e}") + return False return latest_version_parts > current_version_parts + def finish_update_checking(error=''): global is_checking_for_update is_checking_for_update = False - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + if update_needed: + bpy.ops.avatar_toolkit.update_notification_popup('INVOKE_DEFAULT') + ui_refresh() + return None # Important for bpy.app.timers def update_now(latest=False): if latest: @@ -132,6 +195,7 @@ def update_now(latest=False): update_link = version_list[bpy.context.scene.avatar_toolkit_updater_version_list][0] download_file(update_link) + ui_refresh() def download_file(update_url): update_zip_file = os.path.join(downloads_dir, "avatar-toolkit-update.zip") @@ -198,7 +262,9 @@ def finish_update(error=''): print(f"Update failed: {error}") else: print("Update successful!") - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + save_preference("version", latest_version_str) + bpy.ops.avatar_toolkit.restart_blender_popup('INVOKE_DEFAULT') + ui_refresh() def get_version_list(self, context): return [(v, v, '') for v in version_list.keys()] if version_list else [] @@ -219,5 +285,17 @@ def draw_updater_panel(context, layout): row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label')) col.separator() - col.label(text=t('Updater.currentVersion').format(name=get_preference("version"))) + col.label(text=t('Updater.currentVersion').format(name=get_current_version())) +def ui_refresh(): + for windowManager in bpy.data.window_managers: + for window in windowManager.windows: + for area in window.screen.areas: + area.tag_redraw() + +def register(): + bpy.app.handlers.load_post.append(check_for_update_on_start) + +def unregister(): + if check_for_update_on_start in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(check_for_update_on_start) diff --git a/functions/translations.py b/functions/translations.py index e5867b6..4e03f66 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -66,14 +66,14 @@ def load_translations() -> bool: return dictionary != old_dictionary -def t(phrase: str, default: str = None) -> str: +def t(phrase: str, default: str = None, **kwargs) -> str: output: str = dictionary.get(phrase) if output is None: if verbose: print(f'Warning: Unknown phrase: {phrase}') return default if default is not None else phrase # print(f"Translating '{phrase}' to '{output}'") # Debug print - return output + return output.format(**kwargs) if kwargs else output def get_language_display_name(lang: str) -> str: if lang == "auto": From fd87d1d5d79f41386873f63f664a86ff784be0eb Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 22 Oct 2024 00:18:19 +0100 Subject: [PATCH 3/4] Auto update now works --- __init__.py | 5 +++++ core/updater.py | 10 ++-------- resources/translations/en_US.json | 10 ++++++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/__init__.py b/__init__.py index bb1b67e..c628dc6 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,7 @@ if "bpy" not in locals(): from .core.register import __bl_ordered_classes from .core import properties from .core import addon_preferences + from .core.updater import check_for_update_on_start else: import importlib importlib.reload(ui) @@ -33,6 +34,8 @@ def register(): #finally register properties that may use some classes. core.register.register_properties() + bpy.app.handlers.load_post.append(check_for_update_on_start) + from .functions.mesh_tools import AvatarToolkit_OT_ApplyShapeKey bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.separator())) @@ -41,6 +44,8 @@ def register(): def unregister(): print("Unregistering Avatar Toolkit") # Unregister the UI classes + if check_for_update_on_start in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(check_for_update_on_start) # Iterate over the classes to unregister in reverse order and unregister them for cls in reversed(list(__bl_ordered_classes)): diff --git a/core/updater.py b/core/updater.py index 61cee3c..616f3ad 100644 --- a/core/updater.py +++ b/core/updater.py @@ -6,6 +6,7 @@ import urllib import shutil import pathlib import zipfile +import time from threading import Thread from bpy.app.handlers import persistent from ..functions.translations import t @@ -13,7 +14,7 @@ from .addon_preferences import get_preference, get_current_version, save_prefere from .register import register_wrap from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME -GITHUB_REPO = "teamneoneko/Avatar-Toolkit" +GITHUB_REPO = "Yusarina/avito" is_checking_for_update = False update_needed = False @@ -292,10 +293,3 @@ def ui_refresh(): for window in windowManager.windows: for area in window.screen.areas: area.tag_redraw() - -def register(): - bpy.app.handlers.load_post.append(check_for_update_on_start) - -def unregister(): - if check_for_update_on_start in bpy.app.handlers.load_post: - bpy.app.handlers.load_post.remove(check_for_update_on_start) diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 6c3927c..04fa8e6 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -256,8 +256,14 @@ "Updater.UpdateToLatestButton.label": "Update to {name}", "Updater.UpdateToSelectedButton.label": "Update", "Updater.currentVersion": "Current Version: {name}", - "Scene.avatar_toolkit_updater_version_list.name": "Available Versions", - "Scene.avatar_toolkit_updater_version_list.description": "List of available versions to update to", + "Updater.CheckForUpdateButton.desc": "Check for available updates", + "UpdateToLatestButton.desc": "Update to the latest version", + "UpdateNotificationPopup.label": "Update Notification", + "UpdateNotificationPopup.desc": "Notification about available updates", + "UpdateNotificationPopup.newUpdate": "New update available: {version}", + "RestartBlenderPopup.label": "Restart Blender", + "RestartBlenderPopup.desc": "Restart Blender to complete the update", + "RestartBlenderPopup.message": "Update successful! Please restart Blender.", "check_for_update.cantCheck": "Unable to check for updates", "download_file.cantConnect": "Cannot connect to update server", "download_file.cantFindZip": "Update file not found", From 4e18362451011711481db6472291a299dc63d2eb Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 22 Oct 2024 00:27:09 +0100 Subject: [PATCH 4/4] Typing --- core/updater.py | 65 +++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/core/updater.py b/core/updater.py index 616f3ad..b7486d2 100644 --- a/core/updater.py +++ b/core/updater.py @@ -13,17 +13,18 @@ from ..functions.translations import t from .addon_preferences import get_preference, get_current_version, save_preference from .register import register_wrap from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from typing import Dict, List, Tuple, Optional, Set, Any -GITHUB_REPO = "Yusarina/avito" +GITHUB_REPO = "teamneoneko/Avatar-Toolkit" -is_checking_for_update = False -update_needed = False -latest_version = None -latest_version_str = '' -version_list = None +is_checking_for_update: bool = False +update_needed: bool = False +latest_version: Optional[str] = None +latest_version_str: str = '' +version_list: Optional[Dict[str, List[str]]] = None -main_dir = os.path.dirname(os.path.dirname(__file__)) -downloads_dir = os.path.join(main_dir, "downloads") +main_dir: str = os.path.dirname(os.path.dirname(__file__)) +downloads_dir: str = os.path.join(main_dir, "downloads") @register_wrap class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator): @@ -32,7 +33,7 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator): bl_description = t('CheckForUpdateButton.desc') bl_options = {'INTERNAL'} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: check_for_update_background() return {'FINISHED'} @@ -43,7 +44,7 @@ class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator): bl_description = t('UpdateToLatestButton.desc') bl_options = {'INTERNAL'} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: update_now(latest=True) return {'FINISHED'} @@ -54,15 +55,15 @@ class AvatarToolkit_OT_UpdateNotificationPopup(bpy.types.Operator): bl_description = t('UpdateNotificationPopup.desc') bl_options = {'INTERNAL'} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: update_now(latest=True) self.report({'INFO'}, "Update started. Please wait for the process to complete.") return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: return context.window_manager.invoke_props_dialog(self, width=300) - def draw(self, context): + def draw(self, context: bpy.types.Context) -> None: layout = self.layout col = layout.column(align=True) col.label(text=t('UpdateNotificationPopup.newUpdate', default="New update available: {version}").format(version=latest_version_str)) @@ -77,7 +78,7 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel): bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_order = 9 - def draw(self, context): + def draw(self, context: bpy.types.Context) -> None: layout = self.layout draw_updater_panel(context, layout) @@ -88,19 +89,19 @@ class AvatarToolkit_OT_RestartBlenderPopup(bpy.types.Operator): bl_description = t('RestartBlenderPopup.desc', default="Restart Blender to complete the update") bl_options = {'INTERNAL'} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]: return context.window_manager.invoke_props_dialog(self, width=300) - def draw(self, context): + def draw(self, context: bpy.types.Context) -> None: layout = self.layout col = layout.column(align=True) col.label(text=t('RestartBlenderPopup.message', default="Update successful! Please restart Blender.")) @persistent -def check_for_update_on_start(dummy): +def check_for_update_on_start(dummy: Any) -> None: if get_preference("check_for_updates_on_startup", True): current_time = time.time() last_check = get_preference("last_update_check", 0) @@ -108,7 +109,7 @@ def check_for_update_on_start(dummy): check_for_update_background() save_preference("last_update_check", current_time) -def check_for_update_background(): +def check_for_update_background() -> None: global is_checking_for_update if is_checking_for_update: return @@ -116,7 +117,7 @@ def check_for_update_background(): is_checking_for_update = True Thread(target=check_for_update).start() -def check_for_update(): +def check_for_update() -> None: global update_needed, latest_version, latest_version_str, version_list if not get_github_releases(): @@ -133,7 +134,7 @@ def check_for_update(): bpy.app.timers.register(finish_update_checking) -def get_github_releases(): +def get_github_releases() -> bool: global version_list version_list = {} @@ -155,7 +156,7 @@ def get_github_releases(): return True -def check_for_update_available(): +def check_for_update_available() -> bool: global latest_version, latest_version_str if not version_list: return False @@ -181,7 +182,7 @@ def check_for_update_available(): return latest_version_parts > current_version_parts -def finish_update_checking(error=''): +def finish_update_checking(error: str = '') -> None: global is_checking_for_update is_checking_for_update = False if update_needed: @@ -189,7 +190,7 @@ def finish_update_checking(error=''): ui_refresh() return None # Important for bpy.app.timers -def update_now(latest=False): +def update_now(latest: bool = False) -> None: if latest: update_link = version_list[latest_version_str][0] else: @@ -198,7 +199,7 @@ def update_now(latest=False): download_file(update_link) ui_refresh() -def download_file(update_url): +def download_file(update_url: str) -> None: update_zip_file = os.path.join(downloads_dir, "avatar-toolkit-update.zip") if os.path.isdir(downloads_dir): @@ -233,13 +234,13 @@ def download_file(update_url): finish_update() -def find_init_directory(path): +def find_init_directory(path: str) -> Optional[str]: for root, dirs, files in os.walk(path): if "__init__.py" in files: return root return None -def clean_addon_dir(): +def clean_addon_dir() -> None: for item in os.listdir(main_dir): item_path = os.path.join(main_dir, item) if item.startswith('.') or item in ['resources', 'downloads']: @@ -249,7 +250,7 @@ def clean_addon_dir(): elif os.path.isdir(item_path): shutil.rmtree(item_path) -def move_files(from_dir, to_dir): +def move_files(from_dir: str, to_dir: str) -> None: for item in os.listdir(from_dir): s = os.path.join(from_dir, item) d = os.path.join(to_dir, item) @@ -258,7 +259,7 @@ def move_files(from_dir, to_dir): else: shutil.copy2(s, d) -def finish_update(error=''): +def finish_update(error: str = '') -> None: if error: print(f"Update failed: {error}") else: @@ -267,10 +268,10 @@ def finish_update(error=''): bpy.ops.avatar_toolkit.restart_blender_popup('INVOKE_DEFAULT') ui_refresh() -def get_version_list(self, context): +def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]: return [(v, v, '') for v in version_list.keys()] if version_list else [] -def draw_updater_panel(context, layout): +def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None: col = layout.column(align=True) if is_checking_for_update: @@ -288,7 +289,7 @@ def draw_updater_panel(context, layout): col.separator() col.label(text=t('Updater.currentVersion').format(name=get_current_version())) -def ui_refresh(): +def ui_refresh() -> None: for windowManager in bpy.data.window_managers: for window in windowManager.windows: for area in window.screen.areas: