diff --git a/.github/workflows/scripts/update_version.py b/.github/workflows/scripts/update_version.py
deleted file mode 100644
index b5600ab..0000000
--- a/.github/workflows/scripts/update_version.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import argparse
-import os
-import json
-import re
-
-
-def collapse_json(text, list_length=4):
- for length in range(list_length):
- re_pattern = r'\[' + (r'\s*(.+)\s*,' * length)[:-1] + r'\]'
- re_repl = r'[' + ''.join(r'\{}, '.format(i+1) for i in range(length))[:-2] + r']'
-
- text = re.sub(re_pattern, re_repl, text)
-
- return text
-
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser()
-
- def parse_list(text):
- return text.split(",")
- parser.add_argument("-files", type=parse_list, default=[], nargs='?', const=[])
- parser.add_argument("-removed", type=parse_list, default=[], nargs='?', const=[])
- args = parser.parse_args()
-
- addon_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
- version = (0, 0, 0)
- with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file:
- for line in file.readlines():
- if "version" in line:
- version = eval("%s)" % line.split(":")[1].split(")")[0].strip())
- break
- with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file:
- for line in file.readlines():
- if "version" in line:
- check_version = eval(line.split("=")[1].strip())
- if check_version > version:
- version = check_version
- break
-
- print("Update to Version %s\nFiles: %s\nRemoved: %s" % (version, args.files, args.removed))
-
- version = list(version)
- with open(os.path.join(addon_directory, "download_file.json"), 'r', encoding='utf-8') as download_file:
- data = json.loads(download_file.read())
- data_files = data["files"]
- for file in args.files:
- if data_files.get(file, None):
- data_files[file] = version
- data_remove = data["remove"]
- for file in args.removed:
- if file not in data_remove and file.startswith("ActRec/"):
- data_remove.append(file)
- data["version"] = version
- with open(os.path.join(addon_directory, "download_file.json"), 'w', encoding='utf-8') as download_file:
- download_file.write(collapse_json(json.dumps(data, ensure_ascii=False, indent=4)))
-
- lines = []
- with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file:
- for line in file.read().splitlines():
- if "version" in line:
- split = line.split(": ")
- sub_split = split[1].split(")")
- line = "%s: %s%s" % (split[0], str(tuple(version)), sub_split[-1])
- lines.append(line)
- with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'w', encoding='utf-8') as file:
- file.write("\n".join(lines))
- file.write("\n")
-
- lines = []
- with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file:
- for line in file.read().splitlines():
- if "version" in line:
- split = line.split("=")
- line = "version = %s" % str(tuple(version))
- lines.append(line)
- with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'w', encoding='utf-8') as file:
- file.write("\n".join(lines))
- file.write("\n")
diff --git a/.github/workflows/unit_test_addon.yml b/.github/workflows/unit_test_addon.yml
index 857e02e..b7ea94e 100644
--- a/.github/workflows/unit_test_addon.yml
+++ b/.github/workflows/unit_test_addon.yml
@@ -14,9 +14,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- blender-version: ["4.0", "3.6", "3.3"]
+ blender-version: ["4.2", "3.6",]
#os: [ubuntu-latest, windows-latest, macos-latest]
- # FIXME Addon doesn't work on Ubuntu (don't know why)
+ # FIXME Addon tests doesn't work on Ubuntu (don't know why)
os: [windows-latest]
env:
BLENDER_CACHE: ${{ github.workspace }}/.blender_releases_cache # The place where blender releases are downloaded
diff --git a/.github/workflows/update_version.yml b/.github/workflows/update_version.yml
deleted file mode 100644
index 70a375e..0000000
--- a/.github/workflows/update_version.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: Update Version
-
-on:
- push:
- branches: [ master ]
-
-jobs:
-
- update:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 2
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
- - name: Get changed files
- id: files
- run: |
- diff=$(git diff --name-only --diff-filter=AM HEAD^..HEAD | tr '\n' ',')
- echo "added_modified=$diff" >> "$GITHUB_OUTPUT"
- echo "Added & Modified: $diff"
- diff=$(git diff --name-only --diff-filter=D HEAD^..HEAD | tr '\n' ',')
- echo "removed=$diff" >> "$GITHUB_OUTPUT"
- echo "Removed: $diff"
- - name: Update to new Version
- id: update
- working-directory: .github/workflows/scripts
- run: |
- output=$(python update_version.py -files ${{ steps.files.outputs.added_modified }} -removed ${{ steps.files.outputs.removed }})
- output="${output//'%'/'%25'}"
- output="${output//$'\n'/'%0A'}"
- output="${output//$'\r'/'%0D'}"
- echo "log=$output" >> $GITHUB_OUTPUT
- - name: Print Log
- run: echo "${{ steps.update.outputs.log }}"
- - name: Update files on GitHub
- uses: test-room-7/action-update-file@v1.8.0
- with:
- file-path: |
- ActRec/__init__.py
- download_file.json
- ActRec/actrec/config.py
- commit-msg: Update Files
- github-token: ${{ secrets.FILE_UPDATER }}
diff --git a/.gitignore b/.gitignore
index 8680c7f..47581a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ __pycache__/
TextEditorExample.txt
/docs/build
/ActRec/packages
+/ActRec/*.zip
+/ActRec/lib
diff --git a/ActRec/__init__.py b/ActRec/__init__.py
index a925943..e626a2e 100644
--- a/ActRec/__init__.py
+++ b/ActRec/__init__.py
@@ -3,11 +3,11 @@
bl_info = {
"name": "ActionRecorder",
"author": "InamuraJIN, RivinHD",
- "version": (4, 1, 2),
- "blender": (3, 3, 12),
+ "version": (4, 2, 0),
+ "blender": (4, 2, 0),
"location": "View 3D",
"warning": "",
- "docs_url": 'https://github.com/InamuraJIN/ActionRecorder/blob/master/README.md', # Documentation
+ "docs_url": 'https://inamurajin.github.io/ActionRecorder/', # Documentation
"tracker_url": 'https://inamurajin.wixsite.com/website/post/bug-report', # Report Bug
"link": 'https://twitter.com/Inamura_JIN',
"category": "System"
diff --git a/ActRec/actrec/__init__.py b/ActRec/actrec/__init__.py
index 7be8e2f..b69a34c 100644
--- a/ActRec/actrec/__init__.py
+++ b/ActRec/actrec/__init__.py
@@ -1,6 +1,8 @@
# region Imports
# external modules
import json
+import os
+import shutil
# blender modules
import bpy
@@ -10,7 +12,7 @@
# relative imports
# unused import needed to give direct access to the modules
from . import functions, menus, operators, panels, properties, ui_functions, uilist
-from . import config, icon_manager, keymap, log, preferences, update
+from . import config, icon_manager, keymap, log, preferences
from .functions.shared import get_preferences
from . import shared_data
# endregion
@@ -35,6 +37,12 @@ def on_load(dummy=None):
context = bpy.context
ActRec_pref = get_preferences(context)
ActRec_pref.operators_list_length = 0
+
+ # Copy Storage.json for first startup from addon source directory to user directory
+ if not os.path.exists(ActRec_pref.storage_path):
+ source_storage_path = os.path.join(ActRec_pref.source_addon_directory, "Storage.json")
+ shutil.copyfile(source_storage_path, ActRec_pref.storage_path) # copy the file
+
# load local actions
if bpy.data.filepath == "":
try:
@@ -70,6 +78,13 @@ def on_load(dummy=None):
icon_manager.load_icons(ActRec_pref)
log.logger.info("Finished: Load ActRec Data")
+ if bpy.app.version < (4, 2, 0):
+ success = functions.install_wheels(ActRec_pref)
+ if success:
+ log.logger.info("Successfully installed and loaded wheels")
+ else:
+ log.logger.error("Failed to install or load wheels")
+
# region Registration
@@ -81,7 +96,6 @@ def register():
panels.register()
uilist.register()
icon_manager.register()
- update.register()
preferences.register()
keymap.register()
@@ -104,7 +118,6 @@ def unregister():
panels.unregister()
uilist.unregister()
icon_manager.unregister()
- update.unregister()
preferences.unregister()
keymap.unregister()
ui_functions.unregister()
diff --git a/ActRec/actrec/config.py b/ActRec/actrec/config.py
index 76007a2..df6c77e 100644
--- a/ActRec/actrec/config.py
+++ b/ActRec/actrec/config.py
@@ -4,5 +4,4 @@
check_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/download_file.json'
repo_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/%s'
release_notes_url = 'https://github.com/InamuraJIN/ActionRecorder/wiki'
-version = (4, 1, 2)
log_amount = 5
diff --git a/ActRec/actrec/functions/__init__.py b/ActRec/actrec/functions/__init__.py
index 6310967..d8d45b3 100644
--- a/ActRec/actrec/functions/__init__.py
+++ b/ActRec/actrec/functions/__init__.py
@@ -59,9 +59,9 @@
execute_render_complete,
enum_list_id_to_name_dict,
enum_items_to_enum_prop_list,
- install_packages,
get_preferences,
get_categorized_view_3d_modes,
get_attribute,
- get_attribute_default
+ get_attribute_default,
+ install_wheels
)
diff --git a/ActRec/actrec/functions/shared.py b/ActRec/actrec/functions/shared.py
index 3bc44a9..f2becb0 100644
--- a/ActRec/actrec/functions/shared.py
+++ b/ActRec/actrec/functions/shared.py
@@ -1,5 +1,6 @@
# region Imports
# external modules
+import importlib.util
from typing import Optional, Union
from contextlib import suppress
from collections import defaultdict
@@ -10,8 +11,10 @@
import functools
import subprocess
import traceback
+import importlib
from typing import TYPE_CHECKING
from mathutils import Vector, Matrix, Color, Euler, Quaternion
+from ... import __package__ as base_package
# blender modules
import bpy
@@ -32,8 +35,6 @@
Font_analysis = object
# endregion
-__module__ = __package__.split(".")[0]
-
# region functions
@@ -411,7 +412,7 @@ def run_queued_macros(context_copy: dict, action_type: str, action_id: str, star
else:
temp_override = context.temp_override(**context_copy)
with temp_override:
- ActRec_pref = context.preferences.addons[__module__].preferences
+ ActRec_pref = get_preferences(context)
action = getattr(ActRec_pref, action_type)[action_id]
play(context, action.macros, action, action_type, start)
@@ -691,7 +692,7 @@ def play(
action.is_playing = False
-@ persistent
+@persistent
def execute_render_complete(dummy=None) -> None:
# https://docs.blender.org/api/current/bpy.app.handlers.html
"""
@@ -823,87 +824,111 @@ def text_to_lines(context: Context, text: str, font: Font_analysis, limit: int,
total_line_length = start + line_length
total_length = total_line_length + len(psb)
width = sum(characters_width[start: total_length])
+
+ # append to current line
if width <= limit:
lines[-1] += psb
continue
- if sum(characters_width[total_line_length: total_length]) <= limit:
- lines.append(psb)
- start += line_length + len(psb)
- continue
- start += line_length
- while psb != "":
- i = int(bl_math.clamp(limit / width * len(psb), 0, len(psb)))
- if len(psb) != i:
- if sum(characters_width[start: start + i]) <= limit:
- while sum(characters_width[start: start + i]) <= limit:
- i += 1
- i -= 1
- else:
- while sum(characters_width[start: start + i]) >= limit:
- i -= 1
- lines.append(psb[:i])
- psb = psb[i:]
- start += i
- width = sum(characters_width[start: total_length])
+
+ # create new line
+ start = len("".join(lines))
+ lines.append(psb)
+
if (lines[0] == ""):
lines.pop(0)
return lines
-def install_packages(*package_names: list[str]) -> tuple[bool, str]:
+def get_preferences(context: Context) -> AR_preferences:
"""
- install the listed packages and ask for user permission if needed
+ get addon preferences of this addon, which are stored in Blender
Args:
- package_names list[str]: name of the package
+ context (Context): active blender context
Returns:
- tuple[bool, str]: (success, installation output)
+ AR_preferences: preferences of this addon
"""
- try:
- # install package
- output = subprocess.check_output(
- [sys.executable, '-m', 'pip', 'install', *package_names, '--no-color']
- ).decode('utf-8').replace("\r", "")
-
- # get sys.path from pip after installation to update the current sys.path
- output_paths = subprocess.check_output(
- [sys.executable, '-m', 'site']
- ).decode('utf-8').replace("\r", "")
-
- # parse the output to get all sys.path paths
- in_path_list = False
- for line in output_paths.splitlines():
- if line.strip() == "sys.path = [":
- in_path_list = True
- continue
- elif not in_path_list:
- continue
- if line.strip() == "]":
- break
-
- path = line.strip(" \'\",\t").replace("\\\\", "\\")
- if path not in sys.path:
- sys.path.append(path)
+ return context.preferences.addons[base_package].preferences
- return (True, output)
- except (PermissionError, OSError, subprocess.CalledProcessError) as err:
- logger.error(err)
- return (False, err.output)
- return (False, ":(")
-
-
-def get_preferences(context: Context) -> AR_preferences:
+def install_wheels(ActRec_pref: AR_preferences) -> bool:
"""
- get addon preferences of this addon, which are stored in Blender
+ Tires to install the required wheels for the addon and loads them into the system path.
+ If the wheels are already installed it will only add them to the path.
+ NOTE: This function should only be used for Blender prior to version 4.2
Args:
context (Context): active blender context
Returns:
- AR_preferences: preferences of this addon
- """
- return context.preferences.addons[__module__].preferences
+ bool: installation success
+ """
+ # Deprecated This function should be removed if Blender 3.6 LTS is no longer supported
+ def load_packages(installation_path: str) -> bool:
+ """
+ Added the installation path to the system path and tries find the packages.
+
+ Args:
+ installation_path (str): The path were the packages is installed.
+
+ Returns:
+ bool: found the packages successfully
+ """
+ site_package_path = os.path.join(installation_path, "Python310", "site-packages")
+ if site_package_path not in sys.path:
+ sys.path.append(site_package_path)
+ found_packages = (importlib.util.find_spec("fontTools") is not None
+ and importlib.util.find_spec("brotli") is not None)
+ if not found_packages:
+ importlib.invalidate_caches()
+ found_packages = (importlib.util.find_spec("fontTools") is not None
+ and importlib.util.find_spec("brotli") is not None)
+ return found_packages
+
+ installation_path = os.path.join(ActRec_pref.source_addon_directory, "lib")
+
+ if load_packages(installation_path):
+ return True
+
+ package_names = None
+ if sys.platform == "win32":
+ package_names = [
+ "fonttools-4.54.1-cp310-cp310-win_amd64.whl",
+ "Brotli-1.1.0-cp310-cp310-win_amd64.whl"
+ ]
+ elif sys.platform == "darwin":
+ package_names = [
+ "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl",
+ "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl"
+ ]
+ elif sys.platform == "linux":
+ package_names = [
+ "fonttools-4.54.1-py3-none-any.whl",
+ "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64." +
+ "manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
+ ]
+
+ if package_names is None:
+ logger.error("Cannot install required packages as this platform is not supported")
+ return False
+
+ wheel_path = os.path.join(ActRec_pref.source_addon_directory, "wheels")
+ installation_env = {
+ **os.environ,
+ "PYTHONUSERBASE": installation_path
+ }
+ package_paths = [os.path.join(wheel_path, package_name) for package_name in package_names]
+ try:
+ subprocess.run(
+ [sys.executable, '-m', 'pip', 'install', *package_paths, '--no-color', '--user', '--ignore-installed'],
+ env=installation_env
+ )
+ except (PermissionError, OSError, subprocess.CalledProcessError) as err:
+ logger.error(err)
+ return False
+ if load_packages(installation_path):
+ return True
+ return False
# endregion
diff --git a/ActRec/actrec/functions/wrapper.py b/ActRec/actrec/functions/wrapper.py
new file mode 100644
index 0000000..76c6aeb
--- /dev/null
+++ b/ActRec/actrec/functions/wrapper.py
@@ -0,0 +1,44 @@
+# ===============================================================================
+# Wrapper functions for Blender API to support multiple LTS versions of Blender.
+# This script should be independent of any local import to avoid circular imports.
+#
+# Supported Blender versions (Updated: 2024-11-04):
+# - 4.2 LTS
+# - 3.6 LTS
+# ===============================================================================
+
+# region Imports
+import bpy
+from bpy.app import version
+
+import os
+# endregion
+
+# region Functions
+
+
+def get_user_path(package: str, path: str = '', create: bool = False):
+ """
+ Return a user writable directory associated with an extension.
+
+ Args:
+ package (str): The __package__ of the extension.
+ path (str, optional): Optional subdirectory. Defaults to ''.
+ create (bool, optional): Treat the path as a directory and create it if its not existing. Defaults to False.
+ """
+ fallback = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+ try:
+ if version >= (4, 2, 0):
+ return bpy.utils.extension_path_user(package, path=path, create=create)
+ else:
+ return fallback # The Fallback path is also the extension user directory for Blender 3.6 LTS.
+ except ValueError as err:
+ print("ERROR ActRec: ValueError: %s" % str(err))
+ if err.args[0] == "The \"package\" does not name an extension":
+ print("--> This error might be caused as the addon is installed the first time.")
+ print(" If this errors remains please try reinstalling the Add-on and report it to the developer.")
+
+ print(" Fallback to old extension directory: %s." % fallback)
+ return fallback
+
+# endregion
diff --git a/ActRec/actrec/log.py b/ActRec/actrec/log.py
index ced7003..5b2d87f 100644
--- a/ActRec/actrec/log.py
+++ b/ActRec/actrec/log.py
@@ -9,13 +9,14 @@
# blender modules
import bpy
from bpy.app.handlers import persistent
+import addon_utils
+from .. import __package__ as base_package
# relative imports
from . import config
+from .functions import wrapper
# endregion
-__module__ = __package__.split(".")[0]
-
# region Log system
@@ -29,49 +30,61 @@ def __init__(self, count: int) -> None:
Args:
count (int): amount of log files which are kept simultaneously
"""
- dir = self.directory = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs")
+ logger = logging.getLogger(base_package)
+ self.logger = logger
+ logger.setLevel(logging.DEBUG)
+
+ self.setup_file_logging(count)
+
+ sys.excepthook = self.exception_handler
+
+ def setup_file_logging(self, count: int) -> None:
+ """
+ Setup the file logging if possible.
+
+ Args:
+ count (int): amount of log files which are kept simultaneously
+ """
+ dir = self.directory = os.path.join(wrapper.get_user_path(base_package, create=True), "logs")
+
if not os.path.exists(dir):
os.mkdir(dir)
all_logs = os.listdir(dir)
- log_later = []
+ self.log_later = []
while len(all_logs) >= count:
try:
# delete oldest file
os.remove(min([os.path.join(dir, filename) for filename in all_logs], key=os.path.getctime))
except PermissionError as err:
- log_later.append("File is already used -> PermissionError: %s" % str(err))
+ self.log_later.append("File is already used -> PermissionError: %s" % str(err))
break
except FileNotFoundError as err:
- log_later.append("For some reason the File doesn't exists %s" % str(err))
+ self.log_later.append("For some reason the File doesn't exists %s" % str(err))
break
all_logs = os.listdir(dir)
- name = ""
- for arg in sys.argv:
- if arg.endswith(".blend"):
- name = "%s_" % ".".join(os.path.basename(arg).split(".")[:-1])
- self.path = os.path.join(dir, "ActRec_%s%s.log" % (name, datetime.today().strftime('%d-%m-%Y_%H-%M-%S')))
-
- logger = logging.getLogger(__module__)
- self.logger = logger
- logger.setLevel(logging.DEBUG)
-
- logger.info(
- "Logging ActRec %s running on Blender %s"
- % (".".join([str(x) for x in config.version]), bpy.app.version_string)
- )
- for log_text in log_later:
- logger.info(log_text)
-
- sys.excepthook = self.exception_handler
+ self.path = os.path.join(dir, "ActRec_%s.log" % (datetime.today().strftime('%d-%m-%Y_%H-%M-%S')))
def exception_handler(self, exc_type, exc_value, exc_tb) -> None:
traceback.print_exception(exc_type, exc_value, exc_tb)
self.logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
+ def check_file_exists(self) -> bool:
+ """
+ Checks if file logging is possible
+
+ Returns:
+ bool: True if file logging is possible otherwise False
+ """
+
+ return hasattr(self, 'path') and self.path is not None
+
def detach_file(self) -> None:
"""
remove file of the logger
"""
+ if not self.check_file_exists():
+ return
+
self.file_handler.close()
self.logger.removeHandler(self.file_handler)
@@ -79,6 +92,9 @@ def append_file(self) -> None:
"""
adds a file to the logger
"""
+ if not self.check_file_exists():
+ return
+
file_formatter = logging.Formatter(
"%(levelname)s - %(relativeCreated)d - %(filename)s:%(funcName)s - %(message)s"
)
@@ -88,6 +104,25 @@ def append_file(self) -> None:
self.logger.addHandler(file_handler)
self.file_handler = file_handler
+ self.startup_log()
+
+ def startup_log(self):
+ """
+ Logging startup information of the logfile
+ """
+ addon_version = (-1, -1, -1)
+ for mod in addon_utils.modules():
+ if mod.__name__ == base_package:
+ addon_version = mod.bl_info.get("version", addon_version)
+ break
+
+ logger.info(
+ "Logging ActRec %s running on Blender %s"
+ % (".".join([str(x) for x in addon_version]), bpy.app.version_string)
+ )
+ for log_text in self.log_later:
+ logger.info(log_text)
+
def update_log_amount_in_config(amount: int) -> None:
"""
diff --git a/ActRec/actrec/operators/globals.py b/ActRec/actrec/operators/globals.py
index 2259a2e..2684835 100644
--- a/ActRec/actrec/operators/globals.py
+++ b/ActRec/actrec/operators/globals.py
@@ -567,7 +567,7 @@ class AR_OT_global_to_local(shared.Id_based, Operator):
bl_description = "Transfer the selected Action to Local-actions"
bl_options = {'UNDO'}
- @ classmethod
+ @classmethod
def poll(cls, context: Context) -> bool:
ActRec_pref = get_preferences(context)
return len(ActRec_pref.global_actions) and len(ActRec_pref.get("global_actions.selected_ids", []))
diff --git a/ActRec/actrec/operators/macros.py b/ActRec/actrec/operators/macros.py
index e12cd9b..135b3d3 100644
--- a/ActRec/actrec/operators/macros.py
+++ b/ActRec/actrec/operators/macros.py
@@ -497,7 +497,7 @@ class Font_analysis():
def __init__(self, font_path: str) -> None:
self.path = font_path
- if importlib.util.find_spec('fontTools') is None:
+ if not Font_analysis.is_installed():
self.use_dynamic_text = False
return
@@ -517,44 +517,17 @@ def __init__(self, font_path: str) -> None:
@classmethod
def is_installed(cls) -> bool:
-
- return (
- importlib.util.find_spec('fontTools') is not None
- and (bpy.app.version < (3, 4, 0) or importlib.util.find_spec('brotli'))
- )
-
- @classmethod
- def install(cls, logger: Logger) -> bool:
"""
- install fonttools to blender modules if not installed
+ Check if the fontTools and brotli package is installed through its wheels
Returns:
- bool: success
+ bool: True if both packages are installed, otherwise False
"""
- if bpy.app.version >= (3, 4, 0):
- # Blender 3.4 uses woff2 therefore the package brotli is required
- if importlib.util.find_spec('fontTools') is None or importlib.util.find_spec('brotli') is None:
- success, output = functions.install_packages('fontTools', 'brotli')
- if success:
- logger.info(output)
- else:
- logger.warning(output)
- if importlib.util.find_spec('fontTools') is None or importlib.util.find_spec('brotli') is None:
- logger.warning("For some reason fontTools or brotli couldn't be installed :(")
- return False
- else:
- if importlib.util.find_spec('fontTools') is None:
- success, output = functions.install_packages('fontTools')
- if success:
- logger.info(output)
- else:
- logger.warning(output)
-
- if importlib.util.find_spec('fontTools') is None:
- logger.warning("For some reason fontTools couldn't be installed :(")
- return False
- return True
+ return (
+ importlib.util.find_spec('fontTools') is not None
+ and importlib.util.find_spec('brotli') is not None
+ )
def get_width_of_text(self, context: Context, text: str) -> list[float]:
"""
@@ -571,102 +544,11 @@ def get_width_of_text(self, context: Context, text: str) -> list[float]:
total = []
font_style = context.preferences.ui_styles[0].widget
for c in text:
- total.append(self.s[self.t[ord(c)]].width * font_style.points/self.units_per_em)
+ # 0.925 is a constant for fine tuning
+ total.append(self.s[self.t[ord(c)]].width * font_style.points/self.units_per_em * 0.925)
return total
-class AR_OT_macro_multiline_support(Operator):
- bl_idname = "ar.macro_multiline_support"
- bl_label = "Multiline Support"
- bl_options = {'INTERNAL'}
- bl_description = "Adds multiline support the edit macro dialog"
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- return context.window_manager.invoke_props_dialog(self, width=350)
-
- def draw(self, context: Context) -> None:
- ActRec_pref = get_preferences(context)
- layout = self.layout
-
- if Font_analysis.is_installed():
- layout.label(text="Support Enabled")
- return
-
- layout.label(text="Do you want to install multiline support?")
- if bpy.app.version >= (3, 4, 0):
- layout.label(text="This requires the fontTools, brotli package to be installed.")
- else:
- layout.label(text="This requires the fontTools package to be installed.")
- row = layout.row()
- if ActRec_pref.multiline_support_installing:
- if bpy.app.version >= (3, 4, 0):
- row.label(text="Installing fontTools, brotli...")
- else:
- row.label(text="Installing fontTools...")
- else:
- row.operator('ar.macro_install_multiline_support', text="Install")
- row.prop(ActRec_pref, 'multiline_support_dont_ask')
-
- def execute(self, context: Context) -> set[str]:
- bpy.ops.ar.macro_edit("INVOKE_DEFAULT", edit=True, multiline_asked=True)
- return {'FINISHED'}
-
- def cancel(self, context: Context) -> None:
- # Also recall when done is not clicked
- bpy.ops.ar.macro_edit("INVOKE_DEFAULT", edit=True, multiline_asked=True)
-
-
-class AR_OT_macro_install_multiline_support(Operator):
- """
- Try's to install the package fonttools
- to get the width of the given command and split it into multiple lines
- """
- bl_idname = "ar.macro_install_multiline_support"
- bl_label = "Install Multiline Support"
- bl_options = {'INTERNAL'}
-
- success = []
-
- @classmethod
- def poll(cls, context: Context) -> bool:
- ActRec_pref = get_preferences(context)
- return not ActRec_pref.multiline_support_installing
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- def install(success: list, logger):
- success.append(Font_analysis.install(logger))
-
- self.success.clear()
- ActRec_pref = get_preferences(context)
- context.window_manager.modal_handler_add(self)
- self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
- self.thread = threading.Thread(target=install, args=(self.success, logger), daemon=True)
- self.thread.start()
- ActRec_pref.multiline_support_installing = True
- return {'RUNNING_MODAL'}
-
- def modal(self, context: Context, event: Event) -> set[str]:
-
- if event.type != 'TIMER':
- return {'PASS_THROUGH'}
-
- if not self.thread.is_alive():
- return self.execute(context)
- return {'RUNNING_MODAL'}
-
- def execute(self, context: Context) -> set[str]:
- ActRec_pref = get_preferences(context)
- self.thread.join()
- ActRec_pref.multiline_support_installing = False
- if context and context.area:
- context.area.tag_redraw()
- if len(self.success) and self.success[0]:
- self.report({'INFO'}, "Successfully installed multiline support")
- return {'FINISHED'}
- self.report({'ERROR'}, "Could not install multiline support. See Log for further information.")
- return {'CANCELLED'}
-
-
class AR_OT_macro_edit(Macro_based, Operator):
bl_idname = "ar.macro_edit"
bl_label = "Edit"
@@ -716,7 +598,7 @@ def set_command(self, value: str) -> None:
if self.use_last_command:
return
self.lines.clear()
- for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width - 20):
+ for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width):
new = self.lines.add()
new['text'] = line
@@ -744,7 +626,7 @@ def set_last_command(self, value: str) -> None:
if not self.use_last_command:
return
self.lines.clear()
- for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width - 20):
+ for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width):
new = self.lines.add()
new['text'] = line
@@ -776,7 +658,6 @@ def set_use_last_command(self, value: bool) -> None:
last_command: StringProperty(name="Last Command", get=get_last_command, set=set_last_command)
last_id: StringProperty(name="Last Id")
edit: BoolProperty(default=False)
- multiline_asked: BoolProperty(default=False)
clear_operator: BoolProperty(
name="Clear Operator",
description="Delete the parameters of an operator command. Otherwise the complete command is cleared",
@@ -790,7 +671,7 @@ def set_use_last_command(self, value: bool) -> None:
set=set_use_last_command
)
lines: CollectionProperty(type=properties.AR_macro_multiline)
- width: IntProperty(default=500, name="width", description="Window width of the Popup")
+ width: IntProperty(default=750, name="width", description="Window width of the Popup")
font = None
time = 0
is_operator = False
@@ -837,12 +718,6 @@ def invoke(self, context: Context, event: Event) -> set[str]:
index = self.index = functions.get_local_macro_index(action, self.id, self.index)
macro = action.macros[index]
- if not self.multiline_asked and not Font_analysis.is_installed() and not ActRec_pref.multiline_support_dont_ask:
- bpy.ops.ar.macro_multiline_support("INVOKE_DEFAULT")
- self.cancel(context)
- self.multiline_asked = True
- return {'CANCELLED'} # Recall this Operator when handled
-
font_path = functions.get_font_path()
if AR_OT_macro_edit.font is None or AR_OT_macro_edit.font.path != font_path:
AR_OT_macro_edit.font = Font_analysis(font_path)
@@ -959,7 +834,6 @@ def execute(self, context: Context) -> set[str]:
def cancel(self, context: Context) -> None:
self.edit = False
- self.multiline_asked = False
self.use_last_command = False
self.clear()
@@ -1155,9 +1029,7 @@ def handle_button_property(
AR_OT_macro_move_up,
AR_OT_macro_move_down,
AR_OT_macro_edit,
- AR_OT_copy_to_actrec,
- AR_OT_macro_multiline_support,
- AR_OT_macro_install_multiline_support
+ AR_OT_copy_to_actrec
]
# region Registration
diff --git a/ActRec/actrec/panels/main.py b/ActRec/actrec/panels/main.py
index dba3a3f..5f1b4da 100644
--- a/ActRec/actrec/panels/main.py
+++ b/ActRec/actrec/panels/main.py
@@ -5,7 +5,6 @@
# relative imports
from .. import config
-from .. import update
from ..log import log_sys
from ..functions.shared import get_preferences
# endregion
@@ -36,10 +35,6 @@ class AR_PT_local(Panel):
def draw(self, context: Context) -> None:
ActRec_pref = get_preferences(context)
layout = self.layout
- if ActRec_pref.update:
- box = layout.box()
- box.label(text="new Version available (%s)" % ActRec_pref.version)
- update.draw_update_button(box, ActRec_pref)
box = layout.box()
box_row = box.row()
col = box_row.column()
@@ -174,7 +169,6 @@ def draw_header(self, context: Context) -> None:
def draw(self, context: Context) -> None:
layout = self.layout
- ActRec_pref = get_preferences(context)
layout.operator(
'wm.url_open',
text="Manual",
@@ -198,23 +192,6 @@ def draw(self, context: Context) -> None:
'wm.url_open',
text="Release Notes"
).url = config.release_notes_url
- row = layout.row()
- if ActRec_pref.update:
- update.draw_update_button(row, ActRec_pref)
- else:
- row.operator('ar.update_check', text="Check For Updates")
- if ActRec_pref.restart:
- row.operator(
- 'ar.show_restart_menu',
- text="Restart to Finish"
- )
- if ActRec_pref.version != '':
- if ActRec_pref.update:
- layout.label(
- text="new Version available (%s)" % ActRec_pref.version
- )
- else:
- layout.label(text="latest Version (%s)" % ActRec_pref.version)
AR_PT_help.__name__ = "AR_PT_help_%s" % space_type
class AR_PT_advanced(Panel):
diff --git a/ActRec/actrec/preferences.py b/ActRec/actrec/preferences.py
index e7a6fbf..25a6ccb 100644
--- a/ActRec/actrec/preferences.py
+++ b/ActRec/actrec/preferences.py
@@ -1,7 +1,6 @@
# region Imports
# external modules
import os
-import importlib
from typing import TYPE_CHECKING
# blender modules
@@ -11,8 +10,10 @@
import rna_keymap_ui
# relative imports
-from . import properties, functions, config, update, keymap, log, shared_data
+from . import properties, functions, config, keymap, log, shared_data
from .log import logger, log_sys
+from .. import __package__ as base_package
+from .functions import wrapper
if TYPE_CHECKING:
def get_preferences(): return
@@ -24,7 +25,7 @@ def get_preferences(): return
class AR_preferences(AddonPreferences):
- bl_idname = __package__.split(".")[0]
+ bl_idname = base_package
def update_is_loaded(self, context: Context) -> None:
context.scene.name = context.scene.name
@@ -44,17 +45,22 @@ def set_is_loaded(self, value: bool) -> None:
set=set_is_loaded
)
+ source_addon_directory: StringProperty(
+ name="source addon directory",
+ default=os.path.dirname(os.path.dirname(__file__)),
+ get=lambda self: self.bl_rna.properties['source_addon_directory'].default
+ ) # get the base addon directory of the source files
+
addon_directory: StringProperty(
name="addon directory",
- default=os.path.dirname(os.path.dirname(__file__)),
+ default=wrapper.get_user_path(base_package, create=True),
get=lambda self: self.bl_rna.properties['addon_directory'].default
- ) # get the base addon directory
+ ) # get the base addon directory for local files
preference_tab: EnumProperty(
items=[('settings', "Settings", ""),
('path', "Paths", ""),
- ('keymap', "Keymap", ""),
- ('update', "Update", "")],
+ ('keymap', "Keymap", "")],
name="Tab",
description="Switch between preference tabs"
)
@@ -103,7 +109,7 @@ def set_icon_path(self, origin_path: str) -> None:
icon_path: StringProperty(
name="Icons Path",
description="The Path to the Storage for the added Icons",
- default=os.path.join(os.path.dirname(os.path.dirname(__file__)), "Icons"),
+ default=os.path.join(wrapper.get_user_path(base_package, create=True), "Icons"),
get=get_icon_path,
set=set_icon_path
)
@@ -136,6 +142,25 @@ def set_icon_path(self, origin_path: str) -> None:
subtype='PERCENTAGE'
) # used as slider
+ # update
+ update: BoolProperty()
+ restart: BoolProperty()
+ version: StringProperty()
+ auto_update: BoolProperty(
+ default=True,
+ name="Auto Update",
+ description="automatically search for a new update"
+ )
+ update_progress: IntProperty(
+ name="Update Progress",
+ default=-1,
+ min=-1,
+ max=100,
+ soft_min=0,
+ soft_max=100,
+ subtype='PERCENTAGE'
+ ) # used as slider
+
# locals
local_actions: CollectionProperty(type=properties.AR_local_actions)
@@ -271,7 +296,7 @@ def set_storage_path(self, origin_path: str) -> None:
storage_path: StringProperty(
name="Storage Path",
description="The Path to the Storage for the saved Categories",
- default=os.path.join(os.path.dirname(os.path.dirname(__file__)), "Storage.json"),
+ default=os.path.join(wrapper.get_user_path(base_package, create=True), "Storage.json"),
get=get_storage_path,
set=set_storage_path
)
@@ -301,21 +326,7 @@ def draw(self, context: Context) -> None:
col = layout.column()
row = col.row(align=True)
row.prop(ActRec_pref, 'preference_tab', expand=True)
- if ActRec_pref.preference_tab == 'update':
- col.operator('wm.url_open', text="Release Notes").url = config.release_notes_url
- row = col.row()
- if ActRec_pref.update:
- update.draw_update_button(row, ActRec_pref)
- else:
- row.operator('ar.update_check', text="Check For Updates")
- if ActRec_pref.restart:
- row.operator('ar.show_restart_menu', text="Restart to Finish")
- if ActRec_pref.version != '':
- if ActRec_pref.update:
- col.label(text="A new Version is available (%s)" % ActRec_pref.version)
- else:
- col.label(text="You are using the latest Version (%s)" % ActRec_pref.version)
- elif ActRec_pref.preference_tab == 'path':
+ if ActRec_pref.preference_tab == 'path':
col.label(text='Action Storage Folder')
row = col.row()
ops = row.operator(
@@ -395,17 +406,10 @@ def draw(self, context: Context) -> None:
rna_keymap_ui.draw_kmi(kc.keymaps, kc, km, kmi, col2, 0)
elif ActRec_pref.preference_tab == 'settings':
row = col.row()
- row.prop(self, 'auto_update')
row.prop(self, 'autosave')
row = col.row()
row.prop(self, 'hide_local_text')
row.prop(self, 'local_create_empty')
- if importlib.util.find_spec('fontTools') is None:
- row = col.row()
- if self.multiline_support_installing:
- row.label(text="Installing fontTools...")
- else:
- row.operator('ar.macro_install_multiline_support')
col.separator(factor=1.5)
row = col.row()
row.operator('wm.url_open', text="Manual", icon='ASSET_MANAGER').url = config.manual_url
diff --git a/ActRec/actrec/update.py b/ActRec/actrec/update.py
deleted file mode 100644
index a9bd307..0000000
--- a/ActRec/actrec/update.py
+++ /dev/null
@@ -1,546 +0,0 @@
-# region Import
-# external modules
-from typing import Optional, Union
-import requests
-import json
-import os
-import subprocess
-from collections import defaultdict
-import threading
-from contextlib import suppress
-import sys
-from typing import TYPE_CHECKING
-
-# blender modules
-import bpy
-from bpy.types import Operator, Scene, AddonPreferences, UILayout, Context, Event
-from bpy.props import BoolProperty, EnumProperty
-from bpy_extras.io_utils import ExportHelper
-from bpy.app.handlers import persistent
-
-# relative imports
-from . import config
-from .log import logger
-from .functions.shared import get_preferences
-if TYPE_CHECKING:
- from .preferences import AR_preferences
-else:
- AR_preferences = AddonPreferences
-# endregion
-
-
-__module__ = __package__.split(".")[0]
-
-
-class Update_manager:
- """manage data for update processes"""
- download_list = []
- download_length = 0
- update_respond = None
- update_data_chunks = defaultdict(lambda: {"chunks": b''})
- version_file = {} # used to store downloaded file from "AR_OT_update_check"
- version_file_thread = None
-
-# region functions
-
-
-@persistent
-def on_start(dummy: Scene = None) -> None:
- """
- get called on start of Blender with on_load handler and checks for available update of ActRec
- opens a thread to run the process faster (the thread get closed in on_scene_update)
-
- Args:
- dummy (Scene, optional):
- needed because blender handler inputs the scene as argument for a handler function. Defaults to None.
- """
- ActRec_pref = get_preferences(bpy.context)
- if not (ActRec_pref.auto_update and Update_manager.version_file_thread is None):
- return
- t = threading.Thread(target=no_stream_download_version_file, args=[__module__], daemon=True)
- t.start()
- Update_manager.version_file_thread = t
-
-
-@persistent
-def on_scene_update(dummy: Scene = None) -> None:
- """
- get called on the first scene update of Blender and closes the thread from on start,
- which is used to check for updates and removes the functions from the handler
-
- Args:
- dummy (bpy.type.Scene, optional):
- needed because blender handler inputs the scene as argument for a handler function. Defaults to None.
- """
- t = Update_manager.version_file_thread
- if not (t and Update_manager.version_file.get("version", None)):
- return
- t.join()
- bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
- bpy.app.handlers.load_post.remove(on_start)
-
-
-def check_for_update(version_file: Optional[dict]) -> tuple[bool, Union[str, tuple[int, int, int]]]:
- """
- checks if a new version of ActRec is available on GitHub
-
- Args:
- version_file (Optional[dict]): contains data about the addon version and the version of each file
-
- Returns:
- tuple[bool, Union[str, tuple[int, int, int]]]:
- [0] False for error or the latest version, True for new available version;
- [1] error message or tuple of the version
- """
- if version_file is None:
- return (False, "No Internet Connection")
- version = config.version
- download_version = tuple(version_file["version"])
- if download_version > version:
- return (True, download_version)
- else:
- return (False, version)
-
-
-def update(
- ActRec_pref: AR_preferences,
- path: str,
- update_respond: Optional[requests.Response],
- download_chunks: dict,
- download_length: int) -> Optional[bool]:
- """
- runs the update process and shows the download process with a progress bar if possible
-
- Args:
- ActRec_pref (AR_preferences): preferences of this addon
- path (str): path to the file to update
- update_respond (Optional[requests.Response]): open response to file,
- needed if file is to large to download in one function call (chunk size 1024)
- download_chunks (dict): contains all downloaded files and their data
- download_length (int): the current length of the chunks, needed to show progress bar
-
- Returns:
- Optional[bool]: the downloaded file or None if an error occurred
- """
- finished_downloaded = False
- progress = 0
- length = 1
- try:
- if update_respond:
- total_length = update_respond.headers.get('content-length', None)
- if total_length is None:
- length = progress = update_respond.raw._fp_bytes_read
- download_chunks[path]["chunks"] = update_respond.content
- update_respond.close()
- finished_downloaded = True
- Update_manager.update_respond = None
- else:
- length = int(total_length)
- for chunk in update_respond.iter_content(chunk_size=1024):
- if chunk:
- download_chunks[path]["chunks"] += chunk
-
- progress = update_respond.raw._fp_bytes_read
- finished_downloaded = progress == length
- if finished_downloaded:
- update_respond.close()
- Update_manager.update_respond = None
- else:
- Update_manager.update_respond = requests.get(
- config.repo_source_url % path, stream=True)
- ActRec_pref.update_progress = int(100 * (progress / (length * download_length) + (
- download_length - len(Update_manager.download_list)) / download_length))
- if finished_downloaded:
- Update_manager.download_list.pop(0)
- return finished_downloaded
- except Exception as err:
- logger.warning("no Connection (%s)" % err)
- return None
-
-
-def install_update(ActRec_pref: AR_preferences, download_chunks: dict, version_file: dict) -> None:
- """
- installs all downloaded files successively and removes old files if needed
- + cleans up all unused data
-
- Args:
- ActRec_pref (AR_preferences): preferences of this addon
- download_chunks (dict): contains all downloaded files and their data
- version_file (dict): contains data about the addon version, the version of each file and the open request
- """
- for path in download_chunks:
- # remove ActRec/ from path, because the Add-on is inside of another directory on GitHub
- relative_path = path.replace("\\", "/").split("/", 1)[1]
- absolute_path = os.path.join(ActRec_pref.addon_directory, relative_path)
- absolute_directory = os.path.dirname(absolute_path)
- if not os.path.exists(absolute_directory):
- os.makedirs(absolute_directory, exist_ok=True)
- with open(absolute_path, 'w', encoding='utf-8') as ar_file:
- ar_file.write(download_chunks[path]["chunks"].decode('utf-8'))
- for path in version_file['remove']:
- # remove ActRec/ from path, because the Add-on is inside of another directory on GitHub
- relative_path = path.replace("\\", "/").split("/", 1)[1]
- remove_path = os.path.join(ActRec_pref.addon_directory, relative_path)
- if not os.path.exists(remove_path):
- continue
- for root, dirs, files in os.walk(remove_path, topdown=False):
- for name in files:
- os.remove(os.path.join(root, name))
- for name in dirs:
- os.rmdir(os.path.join(root, name))
- version = tuple(ActRec_pref.version.split("."))
- download_chunks.clear()
- version_file.clear()
- logger.info("Updated Action Recorder to Version: %s" % str(version))
-
-
-def start_get_version_file() -> bool:
- """
- starts the request process to get the version file, which is needed to only update the changed files
-
- Returns:
- bool: success of the process
- """
- try:
- Update_manager.version_file['respond'] = requests.get(
- config.check_source_url, stream=True)
- Update_manager.version_file['chunk'] = b''
- logger.info("Start Download: version_file")
- return True
- except Exception as err:
- logger.warning("no Connection (%s)" % err)
- return False
-
-
-def get_version_file(res: requests.Response) -> Union[bool, dict, None]:
- """
- downloads the data of the version file and needed to be called again if the download process isn't finished
-
- Args:
- res (requests.Response): response that was created by start_get_version_file
-
- Returns:
- Union[bool, dict, None]:
- [None] error;
- [True] needed to be called again;
- [dict] data of the version file in JSON-format
- """
- try:
- total_length = res.headers.get('content-length', None)
- if total_length is None:
- logger.info("Finished Download: version_file")
- content = res.content
- res.close()
- return json.loads(content)
-
- for chunk in res.iter_content(chunk_size=1024):
- Update_manager.version_file['chunk'] += chunk
- length = res.raw._fp_bytes_read
- if int(total_length) == length:
- res.close()
- logger.info("Finished Download: version_file")
- return json.loads(Update_manager.version_file['chunk'])
- return True
- except Exception as err:
- logger.warning("no Connection (%s)" % err)
- res.close()
- return None
-
-
-def apply_version_file_result(
- ActRec_pref: AR_preferences,
- version_file: dict,
- update: tuple[bool, Union[tuple, str]]) -> None:
- """
- updates the version in the addon preferences if needed and closes the open request from version file
-
- Args:
- ActRec_pref (AR_preferences): preferences of this addon
- version_file (dict): contains data about the addon version, the version of each file and the open request
- update (tuple[bool, Union[tuple, str]]):
- [0] update is available;
- [1] version of the addon in str or tuple format
- """
- ActRec_pref.update = update[0]
- if not update[0]:
- res = version_file.get('respond')
- if res:
- res.close()
- version_file.clear()
- if isinstance(update[1], str):
- ActRec_pref.version = update[1]
- else:
- ActRec_pref.version = ".".join(map(str, update[1]))
-
-
-def get_download_list(version_file: dict) -> Optional[list]:
- """
- creates a list of which files needed to be downloaded to install the update
-
- Args:
- version_file (dict): contains data about the addon version, the version of each file and the open request
-
- Returns:
- Optional[list]:
- [None] no paths are written;
- [list] list of the paths that needed to be downloaded
- """
- download_files = version_file["files"]
- if download_files is None:
- return None
- download_list = []
- version = config.version
- for key in download_files:
- if tuple(download_files[key]) > version:
- download_list.append(key)
- return download_list
-
-
-def no_stream_download_version_file(module_name: str) -> None:
- """
- downloads the version file without needed to be called again. Is faster but stops Blender from executing other code.
-
- Args:
- module_name (str): name of the addon to get the addon preferences
- """
- try:
- logger.info("Start Download: version_file")
- res = requests.get(config.check_source_url)
- logger.info("Finished Download: version_file")
- Update_manager.version_file = json.loads(res.content)
- version_file = Update_manager.version_file
- update = check_for_update(version_file)
- ActRec_pref = bpy.context.preferences.addons[module_name].preferences
- apply_version_file_result(ActRec_pref, version_file, update)
- except Exception as err:
- logger.warning("no Connection (%s)" % err)
-# endregion functions
-
-# region UI functions
-
-
-def draw_update_button(layout: UILayout, ActRec_pref: AR_preferences) -> None:
- """
- draws the update button and show a progress bar when files get downloaded
-
- Args:
- layout (UILayout): context where to draw the button
- ActRec_pref (AR_preferences): preferences of this addon
- """
- if ActRec_pref.update_progress >= 0:
- row = layout.row()
- row.enabled = False
- row.prop(ActRec_pref, 'update_progress', text="Progress", slider=True)
- else:
- layout.operator('ar.update', text='Update')
-# endregion
-
-# region Operator
-
-
-class AR_OT_update_check(Operator):
- bl_idname = "ar.update_check"
- bl_label = "Check for Update"
- bl_description = "check for available update"
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- res = start_get_version_file()
- if res:
- self.timer = context.window_manager.event_timer_add(0.1)
- context.window_manager.modal_handler_add(self)
- return {'RUNNING_MODAL'}
- self.report({'WARNING'}, "No Internet Connection")
- return {'CANCELLED'}
-
- def modal(self, context: Context, event: Event) -> set[str]:
- version_file = get_version_file(Update_manager.version_file['respond'])
- if isinstance(version_file, dict) or version_file is None:
- Update_manager.version_file = version_file
- return self.execute(context)
- return {'PASS_THROUGH'}
-
- def execute(self, context: Context) -> set[str]:
- version_file = Update_manager.version_file
- if not version_file:
- return {'CANCELLED'}
- if version_file.get('respond'):
- return {'RUNNING_MODAL'}
- update = check_for_update(version_file)
- ActRec_pref = get_preferences(context)
- apply_version_file_result(ActRec_pref, version_file, update)
- context.window_manager.event_timer_remove(self.timer)
- return {"FINISHED"}
-
- def cancel(self, context: Context) -> None:
- if Update_manager.version_file:
- res = Update_manager.version_file.get('respond')
- if res:
- res.close()
- Update_manager.version_file.clear()
- context.window_manager.event_timer_remove(self.timer)
-
-
-class AR_OT_update(Operator):
- bl_idname = "ar.update"
- bl_label = "Update"
- bl_description = "install the new version"
-
- @classmethod
- def poll(cls, context: Context) -> bool:
- ActRec_pref = get_preferences(context)
- return ActRec_pref.update
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- Update_manager.download_list = get_download_list(
- Update_manager.version_file)
- Update_manager.download_length = len(Update_manager.download_list)
- self.timer = context.window_manager.event_timer_add(
- 0.05, window=context.window)
- context.window_manager.modal_handler_add(self)
- return {'RUNNING_MODAL'}
-
- def modal(self, context: Context, event: Event) -> set[str]:
- if not len(Update_manager.download_list):
- return self.execute(context)
-
- ActRec_pref = get_preferences(context)
- path = Update_manager.download_list[0]
- res = update(ActRec_pref, path, Update_manager.update_respond,
- Update_manager.update_data_chunks, Update_manager.download_length)
-
- if res is None:
- self.report({'WARNING'}, "No Internet Connection")
- return {'CANCELLED'}
-
- context.area.tag_redraw()
- return {'PASS_THROUGH'}
-
- def execute(self, context: Context) -> set[str]:
- if not (Update_manager.version_file and Update_manager.update_data_chunks):
- return {'CANCELLED'}
- ActRec_pref = get_preferences(context)
- ActRec_pref.update = False
- ActRec_pref.restart = True
- install_update(ActRec_pref, Update_manager.update_data_chunks,
- Update_manager.version_file)
- ActRec_pref.update_progress = -1
- self.cancel(context)
- bpy.ops.ar.show_restart_menu('INVOKE_DEFAULT')
- context.area.tag_redraw()
- return {"FINISHED"}
-
- def cancel(self, context: Context) -> None:
- res = Update_manager.update_respond
- if res:
- res.close()
- Update_manager.update_respond = None
- Update_manager.download_length = 0
- Update_manager.download_list.clear()
- Update_manager.update_data_chunks.clear()
- context.window_manager.event_timer_remove(self.timer)
-
-
-class AR_OT_restart(Operator, ExportHelper):
- bl_idname = "ar.restart"
- bl_label = "Restart Blender"
- bl_description = "Restart Blender"
- bl_options = {"INTERNAL"}
-
- save: BoolProperty(default=False)
- filename_ext = ".blend"
- filter_folder: BoolProperty(default=True, options={'HIDDEN'})
- filter_blender: BoolProperty(default=True, options={'HIDDEN'})
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- if self.save and not bpy.data.filepath:
- return ExportHelper.invoke(self, context, event)
- else:
- return self.execute(context)
-
- def execute(self, context: Context) -> set[str]:
- ActRec_pref = get_preferences(context)
- path = bpy.data.filepath
- if self.save:
- if not path:
- path = self.filepath
- if not path:
- return ExportHelper.invoke(self, context, None)
- bpy.ops.wm.save_mainfile(filepath=path)
- ActRec_pref.restart = False
- if os.path.exists(path):
- args = [*sys.argv, path]
- else:
- args = sys.argv
- subprocess.Popen(args)
- bpy.ops.wm.quit_blender()
- return {"FINISHED"}
-
- def draw(self, context: Context) -> None:
- pass
-
-
-class AR_OT_show_restart_menu(Operator):
- bl_idname = "ar.show_restart_menu"
- bl_label = "Restart Blender"
- bl_description = "Restart Blender"
- bl_options = {'REGISTER', 'UNDO'}
-
- restart_options: EnumProperty(
- items=[("exit", "Don't Restart", "Don't restart and exit this window"),
- ("save", "Save & Restart", "Save & Restart Blender"),
- ("restart", "Restart", "Restart Blender")])
-
- def invoke(self, context: Context, event: Event) -> set[str]:
- return context.window_manager.invoke_props_dialog(self)
-
- def execute(self, context: Context) -> set[str]:
- if self.restart_options == "save":
- bpy.ops.ar.restart(save=True)
- elif self.restart_options == "restart":
- bpy.ops.ar.restart()
- return {"FINISHED"}
-
- def cancel(self, context: Context) -> None:
- bpy.ops.ar.show_restart_menu("INVOKE_DEFAULT")
-
- def draw(self, context: Context) -> None:
- ActRec_pref = get_preferences(context)
- layout = self.layout
- if ActRec_pref.restart:
- layout.label(
- text="You need to restart Blender to complete the Update")
- layout.prop(self, 'restart_options', expand=True)
-# endregion
-
-
-classes = [
- AR_OT_update_check,
- AR_OT_update,
- AR_OT_restart,
- AR_OT_show_restart_menu
-]
-
-# region Registration
-
-
-def register():
- for cls in classes:
- bpy.utils.register_class(cls)
- bpy.app.handlers.load_post.append(on_start)
- bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
-
-
-def unregister():
- for cls in classes:
- bpy.utils.unregister_class(cls)
- with suppress(Exception):
- bpy.app.handlers.load_post.remove(on_start)
- with suppress(Exception):
- bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
- Update_manager.download_list.clear()
- Update_manager.download_length = 0
- Update_manager.update_data_chunks.clear()
- Update_manager.update_respond = None
- Update_manager.version_file.clear()
- Update_manager.version_file_thread = None
-# endregion
diff --git a/ActRec/blender_manifest.toml b/ActRec/blender_manifest.toml
new file mode 100644
index 0000000..8c0e867
--- /dev/null
+++ b/ActRec/blender_manifest.toml
@@ -0,0 +1,78 @@
+schema_version = "1.0.0"
+
+# Example of manifest file for a Blender extension
+# Change the values according to your extension
+id = "action_recorder"
+version = "4.2.0"
+name = "Action Recorder"
+tagline = "Automate complex, repetitive tasks to improve efficiency"
+maintainer = "InamuraJIN, RivinHD"
+# Supported types: "add-on", "theme"
+type = "add-on"
+
+# # Optional: link to documentation, support, source files, etc
+website = "https://inamurajin.github.io/ActionRecorder/"
+
+# # Optional: tag list defined by Blender and server, see:
+# # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
+tags = ["User Interface", "System"]
+
+blender_version_min = "4.2.0"
+# # Optional: Blender version that the extension does not support, earlier versions are supported.
+# # This can be omitted and defined later on the extensions platform if an issue is found.
+# blender_version_max = "5.1.0"
+
+# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
+# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
+license = [
+ "SPDX:GPL-3.0-or-later",
+]
+# # Optional: required by some licenses.
+# copyright = [
+# "2002-2024 Developer Name",
+# "1998 Company Name",
+# ]
+
+# # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems.
+platforms = ["windows-x64", "macos-arm64", "linux-x64", "macos-x64"]
+# # Other supported platforms: "windows-arm64"
+
+# Optional: bundle 3rd party Python modules.
+# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
+wheels = [
+ "./wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl",
+ "./wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl",
+ "./wheels/fonttools-4.54.1-py3-none-any.whl",
+ "./wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl",
+ "./wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl",
+ "./wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+]
+
+# # Optional: add-ons can list which resources they will require:
+# # * files (for access of any filesystem operations)
+# # * network (for internet access)
+# # * clipboard (to read and/or write the system clipboard)
+# # * camera (to capture photos and videos)
+# # * microphone (to capture audio)
+# #
+# # If using network, remember to also check `bpy.app.online_access`
+# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
+# #
+# # For each permission it is important to also specify the reason why it is required.
+# # Keep this a single short sentence without a period (.) at the end.
+# # For longer explanations use the documentation or detail page.
+[permissions]
+files = "Save/Load the global actions, custom icons and log file"
+
+# # Optional: advanced build settings.
+# # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
+# # These are the default build excluded patterns.
+# # You only need to edit them if you want different options.
+[build]
+paths_exclude_pattern = [
+ "__pycache__/",
+ "/.git/",
+ "/*.zip",
+ "/Icons/",
+ "/logs/",
+]
diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl
new file mode 100644
index 0000000..29ff0d4
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl differ
diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
new file mode 100644
index 0000000..860a205
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl differ
diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl
new file mode 100644
index 0000000..ebb6c09
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl differ
diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl
new file mode 100644
index 0000000..51bb597
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl differ
diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
new file mode 100644
index 0000000..5ee808e
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ
diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl
new file mode 100644
index 0000000..c8bcf38
Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl differ
diff --git a/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl
new file mode 100644
index 0000000..8876343
Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl differ
diff --git a/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl
new file mode 100644
index 0000000..c838fc1
Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl differ
diff --git a/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl
new file mode 100644
index 0000000..6b6fd8f
Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl differ
diff --git a/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl
new file mode 100644
index 0000000..8d5096e
Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl differ
diff --git a/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl b/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl
new file mode 100644
index 0000000..7ad4b94
Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl differ
diff --git a/README.md b/README.md
index 2ec2673..5e2687c 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,16 @@
+# Website (Installation & Documentation)
+**https://inamurajin.github.io/ActionRecorder/**
+
# LINK
- - [Action Recorder](#action-recorder)
- - [Add-ons Explained](#add-ons-explained)
- - [INSTALL](#install)
- - [Tutorial&Readme](#tutorialreadme)
- - [Update](#update)
- - [Community](#community)
- - [Bug Reports and Requests](#bug-reports-and-requests)
-# PLEASE!
-Our workload has made it difficult for us to update and fix bugs
-New programmers are expected to be created!
-We have therefore made some major changes to the Action Recorder add-on
-For example, we have separated the Python files and added comments to make it easier for other programmers to understand
-Please Fork this add-on!
+- [Website (Installation \& Documentation)](#website-installation--documentation)
+- [LINK](#link)
+- [Action Recorder](#action-recorder)
+- [Add-ons Explained](#add-ons-explained)
+- [INSTALL](#install)
+- [Old Tutorial\&Readme](#old-tutorialreadme)
+- [Update](#update)
+- [Community](#community)
+- [Bug Reports and Requests](#bug-reports-and-requests)
# Action Recorder
Are you tirred of coding long intimidating coding task just for one-off modeling? Or may be you're not good coding at all !
@@ -37,7 +36,7 @@ Convert complex, repetitive tasks into one click!
You have to remember is "Add" & "Play" Only!
-Supported Versions: 2.83 to 2.9
+Supported Versions: 3.6 to 4.2
Simplify complex repetitive tasks, which were difficult to do with the standard “Repeat Last”(Shift+R) function of Blender, by registering multiple actions.
@@ -63,7 +62,7 @@ Click the "Install Add-on" button
このページの右側に「Release」ボタンからZipファイルをダウンロードして下さい
-# Tutorial&Readme
+# Old Tutorial&Readme
🇯🇵[日本語](https://inamurajin.wixsite.com/website/post/tutorial_readme_jp)
※ 日本語解説の更新は停止します。前バージョンの記事で参考に出来ますが、詳しくは英語版でご覧ください
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..623c61f
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,2 @@
+blender --command extension build
+blender --command extension build --split-platforms
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 682fdf6..a5ff690 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -26,7 +26,8 @@
'sphinx.ext.napoleon',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
- 'myst_parser'
+ 'myst_parser',
+ 'sphinx_copybutton'
]
templates_path = ['_templates']
@@ -45,7 +46,7 @@
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-html_theme = 'sphinx_rtd_theme'
+html_theme = 'furo'
html_static_path = ['_static']
html_context = {
"default_mode": "auto"
diff --git a/docs/source/getting_started/terms_definition.md b/docs/source/getting_started/terms_definition.md
index 6ca7319..13a5335 100644
--- a/docs/source/getting_started/terms_definition.md
+++ b/docs/source/getting_started/terms_definition.md
@@ -21,7 +21,12 @@ By default it is saved in the `Storage.json` file which is located inside the in
:::{hint}
By default the path to the `Storage`-File is\
-`
+```
C:\Users\\AppData\Roaming\Blender Foundation\Blender\3.3\scripts\addons\ActRec\Storage.json
-`
+```
+
+As of Blender version 4.2 the `Storage`-File is located in the extension user directory:\
+```
+C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.user\user_default\action_recorder\Storage.json<>
+```
:::
\ No newline at end of file
diff --git a/docs/source/panels/macro.md b/docs/source/panels/macro.md
index 4034947..10332ca 100644
--- a/docs/source/panels/macro.md
+++ b/docs/source/panels/macro.md
@@ -32,7 +32,7 @@ Copies the command to the clipboard.
### Execution Context
The context which the operator will be executed with.
-Most of the time `Execution` is the right selection.
+Most of the time `Execute` is the right selection.
:::{Hint}
Sometimes it is helpful to set it to `Invoke` where the operator can access user input or startup a process that will not be executed immediately.
@@ -96,26 +96,4 @@ Executes all macros from the selected Action (Shortcut: `alt + .`).
### Local to Global
Moves the selected Action to the Global Panel. A popup appears to select the Category to append the Action to.
-The "Settings/Buttons" below `Local to Global` decided weather to `Copy` the Action (keep this Action in `Local` and move an exact copy it to the `Global` section) or `Move` (removes this action from `Local` and append it to the `Global` section).
-
-## Multiline Support
-
-:::{figure-md}
-
-
-First Popup to install Multiline Support
-:::
-
-If `Don't Ask Again` is checked it can be later installed in the Preferences
-
-:::{figure-md}
-
-
-Later install in the Preferences
-:::
-
-If Install is pressed the Popup will change to the following:\
-
-
-After the installation finished the following appear:\
-
\ No newline at end of file
+The "Settings/Buttons" below `Local to Global` decided weather to `Copy` the Action (keep this Action in `Local` and move an exact copy it to the `Global` section) or `Move` (removes this action from `Local` and append it to the `Global` section).
\ No newline at end of file
diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt
index cf9a675..0190c42 100644
--- a/docs/source/requirements.txt
+++ b/docs/source/requirements.txt
@@ -1,2 +1,3 @@
-sphinx-rtd-theme
-myst-parser
\ No newline at end of file
+furo
+myst-parser
+sphinx-copybutton
\ No newline at end of file
diff --git a/download_file.json b/download_file.json
deleted file mode 100644
index 3c88f3a..0000000
--- a/download_file.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "version": [4, 1, 2],
- "files": {
- "ActRec/actrec/functions/__init__.py": [4, 1, 0],
- "ActRec/actrec/functions/categories.py": [4, 1, 0],
- "ActRec/actrec/functions/globals.py": [4, 1, 0],
- "ActRec/actrec/functions/locals.py": [4, 1, 0],
- "ActRec/actrec/functions/macros.py": [4, 1, 1],
- "ActRec/actrec/functions/shared.py": [4, 1, 2],
- "ActRec/actrec/menus/__init__.py": [4, 0, 6],
- "ActRec/actrec/menus/locals.py": [4, 1, 0],
- "ActRec/actrec/menus/categories.py": [4, 1, 0],
- "ActRec/actrec/operators/__init__.py": [4, 1, 0],
- "ActRec/actrec/operators/categories.py": [4, 1, 0],
- "ActRec/actrec/operators/globals.py": [4, 1, 2],
- "ActRec/actrec/operators/helper.py": [4, 1, 0],
- "ActRec/actrec/operators/locals.py": [4, 1, 0],
- "ActRec/actrec/operators/macros.py": [4, 1, 2],
- "ActRec/actrec/operators/preferences.py": [4, 1, 0],
- "ActRec/actrec/operators/shared.py": [4, 1, 0],
- "ActRec/actrec/panels/__init__.py": [4, 0, 0],
- "ActRec/actrec/panels/main.py": [4, 1, 0],
- "ActRec/actrec/properties/__init__.py": [4, 0, 6],
- "ActRec/actrec/properties/categories.py": [4, 0, 0],
- "ActRec/actrec/properties/globals.py": [4, 1, 0],
- "ActRec/actrec/properties/locals.py": [4, 1, 0],
- "ActRec/actrec/properties/macros.py": [4, 1, 0],
- "ActRec/actrec/properties/shared.py": [4, 1, 0],
- "ActRec/actrec/ui_functions/__init__.py": [4, 0, 6],
- "ActRec/actrec/ui_functions/categories.py": [4, 1, 0],
- "ActRec/actrec/ui_functions/globals.py": [4, 1, 0],
- "ActRec/actrec/uilist/__init__.py": [4, 0, 0],
- "ActRec/actrec/uilist/locals.py": [4, 1, 0],
- "ActRec/actrec/uilist/macros.py": [4, 1, 0],
- "ActRec/actrec/__init__.py": [4, 0, 8],
- "ActRec/actrec/config.py": [4, 1, 2],
- "ActRec/actrec/icon_manager.py": [4, 1, 0],
- "ActRec/actrec/keymap.py": [4, 1, 2],
- "ActRec/actrec/log.py": [4, 1, 0],
- "ActRec/actrec/preferences.py": [4, 1, 0],
- "ActRec/actrec/shared_data.py": [4, 0, 8],
- "ActRec/actrec/update.py": [4, 1, 0],
- "ActRec/__init__.py": [4, 1, 2]
- },
- "remove": []
-}
\ No newline at end of file
diff --git a/update_wheels.sh b/update_wheels.sh
new file mode 100644
index 0000000..c63afff
--- /dev/null
+++ b/update_wheels.sh
@@ -0,0 +1,16 @@
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_10_13_universal2
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=manylinux_2_28_x86_64
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
+
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_10_13_universal2
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=manylinux_2_17_x86_64
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
+
+# Deprecated: removed the command below if the blender version 3.6 LTS is no longer supported
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=macosx_10_13_universal2
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=manylinux_2_28_x86_64
+pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=win_amd64
+
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=macosx_10_13_universal2
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=manylinux1_x86_64
+pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=win_amd64
\ No newline at end of file