346f73ebb0
- Fixes issue where console was getting spammed re armature selection. - Fixes apply as rest pose issues and improves the system. https://github.com/teamneoneko/Avatar-Toolkit/issues/52 - Fixes Apply pose to shapekey, it does what it's meant to do now. https://github.com/teamneoneko/Avatar-Toolkit/issues/52 - Fixes error in updater when there no updates to list.
301 lines
10 KiB
Python
301 lines
10 KiB
Python
import bpy
|
|
import os
|
|
import ssl
|
|
import json
|
|
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
|
|
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 = "teamneoneko/Avatar-Toolkit"
|
|
|
|
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: 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):
|
|
bl_idname = 'avatar_toolkit.check_for_update'
|
|
bl_label = t('CheckForUpdateButton.label')
|
|
bl_description = t('CheckForUpdateButton.desc')
|
|
bl_options = {'INTERNAL'}
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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: 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: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
|
return context.window_manager.invoke_props_dialog(self, width=300)
|
|
|
|
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))
|
|
|
|
@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 = 9
|
|
|
|
def draw(self, context: bpy.types.Context) -> None:
|
|
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: bpy.types.Context) -> Set[str]:
|
|
return {'FINISHED'}
|
|
|
|
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: 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: Any) -> None:
|
|
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() -> None:
|
|
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() -> None:
|
|
global update_needed, latest_version, latest_version_str, version_list
|
|
|
|
if not get_github_releases():
|
|
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.')
|
|
|
|
bpy.app.timers.register(finish_update_checking)
|
|
|
|
def get_github_releases() -> bool:
|
|
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() -> bool:
|
|
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_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: str = '') -> None:
|
|
global is_checking_for_update
|
|
is_checking_for_update = False
|
|
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: bool = False) -> None:
|
|
if not version_list:
|
|
print("No version list available. Please check for updates first.")
|
|
return
|
|
|
|
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)
|
|
ui_refresh()
|
|
|
|
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):
|
|
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: str) -> Optional[str]:
|
|
for root, dirs, files in os.walk(path):
|
|
if "__init__.py" in files:
|
|
return root
|
|
return None
|
|
|
|
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']:
|
|
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: 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)
|
|
if os.path.isdir(s):
|
|
shutil.copytree(s, d, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copy2(s, d)
|
|
|
|
def finish_update(error: str = '') -> None:
|
|
if error:
|
|
print(f"Update failed: {error}")
|
|
else:
|
|
print("Update successful!")
|
|
save_preference("version", latest_version_str)
|
|
bpy.ops.avatar_toolkit.restart_blender_popup('INVOKE_DEFAULT')
|
|
ui_refresh()
|
|
|
|
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: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
|
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_current_version()))
|
|
|
|
def ui_refresh() -> None:
|
|
for windowManager in bpy.data.window_managers:
|
|
for window in windowManager.windows:
|
|
for area in window.screen.areas:
|
|
area.tag_redraw()
|