@@ -7,6 +7,7 @@ if "bpy" not in locals():
|
|||||||
from .core.register import __bl_ordered_classes
|
from .core.register import __bl_ordered_classes
|
||||||
from .core import properties
|
from .core import properties
|
||||||
from .core import addon_preferences
|
from .core import addon_preferences
|
||||||
|
from .core.updater import check_for_update_on_start
|
||||||
else:
|
else:
|
||||||
import importlib
|
import importlib
|
||||||
importlib.reload(ui)
|
importlib.reload(ui)
|
||||||
@@ -33,6 +34,8 @@ def register():
|
|||||||
#finally register properties that may use some classes.
|
#finally register properties that may use some classes.
|
||||||
core.register.register_properties()
|
core.register.register_properties()
|
||||||
|
|
||||||
|
bpy.app.handlers.load_post.append(check_for_update_on_start)
|
||||||
|
|
||||||
from .functions.mesh_tools import AvatarToolkit_OT_ApplyShapeKey
|
from .functions.mesh_tools import AvatarToolkit_OT_ApplyShapeKey
|
||||||
|
|
||||||
bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.separator()))
|
bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.separator()))
|
||||||
@@ -41,6 +44,8 @@ def register():
|
|||||||
def unregister():
|
def unregister():
|
||||||
print("Unregistering Avatar Toolkit")
|
print("Unregistering Avatar Toolkit")
|
||||||
# Unregister the UI classes
|
# 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
|
# Iterate over the classes to unregister in reverse order and unregister them
|
||||||
for cls in reversed(list(__bl_ordered_classes)):
|
for cls in reversed(list(__bl_ordered_classes)):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "4.3.1"
|
version = "0.1.0"
|
||||||
name = "Avatar Toolkit"
|
name = "Avatar Toolkit"
|
||||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||||
maintainer = "Team NekoNeo"
|
maintainer = "Team NekoNeo"
|
||||||
|
|||||||
+986
@@ -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
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import os
|
import os
|
||||||
|
import toml
|
||||||
import json
|
import json
|
||||||
from bpy.types import AddonPreferences
|
from bpy.types import AddonPreferences
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
@@ -8,6 +9,13 @@ from typing import Any, Dict
|
|||||||
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
|
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:
|
def save_preference(key: str, value: Any) -> None:
|
||||||
"""Save a single preference to the JSON file."""
|
"""Save a single preference to the JSON file."""
|
||||||
prefs = load_preferences()
|
prefs = load_preferences()
|
||||||
|
|||||||
+7
-1
@@ -5,6 +5,7 @@ from bpy.types import Scene, Object, Material, Context
|
|||||||
from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
|
from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
|
||||||
from ..core.addon_preferences import get_preference
|
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 ..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:
|
def register() -> None:
|
||||||
default_language = get_preference("language", 0)
|
default_language = get_preference("language", 0)
|
||||||
@@ -74,6 +75,12 @@ def register() -> None:
|
|||||||
name=t("Quick_Access.selected_armature.label"),
|
name=t("Quick_Access.selected_armature.label"),
|
||||||
description=t("Quick_Access.selected_armature.desc")
|
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
|
#happy with how compressed this get_texture_node_list method is - @989onan
|
||||||
def get_texture_node_list(self: Material, context: Context) -> list[set[3]]:
|
def get_texture_node_list(self: Material, context: Context) -> list[set[3]]:
|
||||||
@@ -131,7 +138,6 @@ def register() -> None:
|
|||||||
get=MaterialListBool.get_bool,
|
get=MaterialListBool.get_bool,
|
||||||
set=MaterialListBool.set_bool)))
|
set=MaterialListBool.set_bool)))
|
||||||
|
|
||||||
|
|
||||||
def unregister() -> None:
|
def unregister() -> None:
|
||||||
#if you register properties with register_property then you shouldn't need this function.
|
#if you register properties with register_property then you shouldn't need this function.
|
||||||
pass
|
pass
|
||||||
|
|||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
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 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()
|
||||||
@@ -66,14 +66,14 @@ def load_translations() -> bool:
|
|||||||
|
|
||||||
return dictionary != old_dictionary
|
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)
|
output: str = dictionary.get(phrase)
|
||||||
if output is None:
|
if output is None:
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f'Warning: Unknown phrase: {phrase}')
|
print(f'Warning: Unknown phrase: {phrase}')
|
||||||
return default if default is not None else phrase
|
return default if default is not None else phrase
|
||||||
# print(f"Translating '{phrase}' to '{output}'") # Debug print
|
# 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:
|
def get_language_display_name(lang: str) -> str:
|
||||||
if lang == "auto":
|
if lang == "auto":
|
||||||
|
|||||||
@@ -249,7 +249,25 @@
|
|||||||
"MMDOptions.renaming_bones": "Renaming bones",
|
"MMDOptions.renaming_bones": "Renaming bones",
|
||||||
"MMDOptions.armature_optimization_complete": "Armature optimization complete",
|
"MMDOptions.armature_optimization_complete": "Armature optimization complete",
|
||||||
"MMDOptions.convert_materials.label": "Convert Materials",
|
"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}",
|
||||||
|
"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",
|
||||||
|
"download_file.cantFindAvatarToolkit": "Avatar Toolkit files not found in update package"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user