work
save startup blend for animation tab & whatnot
This commit is contained in:
@@ -9,10 +9,12 @@ The original method is then called from the new method, with the same arguments,
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
|
||||
from . import icons
|
||||
from . import icons, version_compare
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
import bl_pkg.bl_extension_ui as exui
|
||||
from bpy.props import IntProperty, StringProperty
|
||||
@@ -23,6 +25,57 @@ EXTENSIONS_API_URL = "https://www.blenderkit.com/api/v1/extensions/"
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
# Per-draw-cycle caching for ensure_repo_cache() to avoid repeated
|
||||
# filesystem stat calls when drawing hundreds of extension items.
|
||||
_repo_cache_last_check: float = 0.0
|
||||
_repo_cache_last_result: bool = False
|
||||
_REPO_CACHE_CHECK_INTERVAL: float = 3.0 # seconds between filesystem checks
|
||||
|
||||
# Cached repository reference to avoid iterating repos per item.
|
||||
_cached_repository = None
|
||||
_cached_repository_time: float = 0.0
|
||||
_REPO_LOOKUP_INTERVAL: float = 5.0 # seconds between repo lookups
|
||||
|
||||
# Cache for price padding strings to avoid repeated blf.dimensions() calls.
|
||||
_price_padding_cache: dict = {}
|
||||
|
||||
|
||||
def get_perfect_price_padding(price_str: str, target_length: int = 70) -> str:
|
||||
"""Generate a padding string to align price text nicely in the UI."""
|
||||
cache_key = (price_str, target_length)
|
||||
cached = _price_padding_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
spaces = [
|
||||
(19, "\u2003"), # em space = U+2003 > 19 units
|
||||
(2, "\u200a"), # hair space = U+200A > 2 units
|
||||
(3, "\u2009"), # 6 em space = U+2009 > 3 units
|
||||
(5, "\u2005"), # 4 em space = U+2005 > 5 units
|
||||
(6, "\u2004"), # 3 em space = U+2004 > 6 units
|
||||
]
|
||||
out = ""
|
||||
size = blf.dimensions(0, price_str)
|
||||
w_size = size[0]
|
||||
final_size = target_length - w_size
|
||||
if final_size <= 0:
|
||||
_price_padding_cache[cache_key] = out
|
||||
return out
|
||||
while w_size < target_length:
|
||||
for spc_len, spc_char in spaces:
|
||||
if w_size + spc_len <= target_length:
|
||||
out += spc_char
|
||||
w_size += spc_len
|
||||
break
|
||||
else:
|
||||
break # No suitable space found, exit loop
|
||||
# double check if we are exporting only white spaces to prevent issues
|
||||
if re.fullmatch(r"\s*", out) is None:
|
||||
_price_padding_cache[cache_key] = ""
|
||||
return ""
|
||||
_price_padding_cache[cache_key] = out
|
||||
return out
|
||||
|
||||
|
||||
# --- New Modal Operator ---
|
||||
class BK_OT_buy_extension_and_watch(Operator):
|
||||
@@ -278,23 +331,47 @@ def extension_draw_item_blenderkit(
|
||||
if pkg_block is not None:
|
||||
row_right.label(text="Blocked ")
|
||||
elif is_installed:
|
||||
if is_outdated:
|
||||
# double check if we are truly outdated
|
||||
# local repo can have a newer version than remote if user installed a dev version
|
||||
# so we compare versions here again
|
||||
local_older = version_compare.compare_versions(
|
||||
item_local.version, item_remote.version
|
||||
)
|
||||
if local_older < 0:
|
||||
props = row_right.operator("extensions.package_install", text="Update")
|
||||
props.repo_index = repo_index
|
||||
props.pkg_id = pkg_id
|
||||
props.enable_on_install = is_enabled
|
||||
elif local_older == 0:
|
||||
row_right.label(text="Up to date ")
|
||||
else:
|
||||
props = row_right.operator(
|
||||
"extensions.package_install",
|
||||
text=f"Downgrade {item_remote.version}",
|
||||
)
|
||||
props.repo_index = repo_index
|
||||
props.pkg_id = pkg_id
|
||||
props.enable_on_install = is_enabled
|
||||
else:
|
||||
### BlenderKit specific code
|
||||
# blenderkit logo icon
|
||||
pcoll = icons.icon_collections["main"]
|
||||
icon_value = pcoll["logo"].icon_id
|
||||
# row.label(text="", icon_value=icon_value)
|
||||
|
||||
# only enable install for those for whom it's available
|
||||
if bk_cache_pkg is not None:
|
||||
can_download_value = bk_cache_pkg.get("can_download")
|
||||
is_for_sale_flag = bk_cache_pkg.get("is_for_sale") is True
|
||||
is_free_flag = bk_cache_pkg.get("is_free") is True
|
||||
|
||||
# special case for blenderkit addon itself
|
||||
if pkg_id == "blenderkit":
|
||||
can_download_value = True
|
||||
|
||||
# Free , purchased and subscribed add-ons, probably also private add-ons
|
||||
if bk_cache_pkg.get("can_download") is True:
|
||||
if can_download_value is True:
|
||||
# if the addon is also for sale, it means the user purchased it and we write "install purchased"
|
||||
if bk_cache_pkg.get("is_for_sale") is True:
|
||||
if is_for_sale_flag:
|
||||
props = row_right.operator(
|
||||
"extensions.package_install",
|
||||
text="Install purchased",
|
||||
@@ -309,14 +386,13 @@ def extension_draw_item_blenderkit(
|
||||
props.repo_index = repo_index
|
||||
props.pkg_id = pkg_id
|
||||
|
||||
# Full plan addons
|
||||
elif not bk_cache_pkg.get("is_free") and not bk_cache_pkg.get(
|
||||
"is_for_sale"
|
||||
# Free addon but limited to full plan
|
||||
elif (can_download_value == "Rejected in this plan") or (
|
||||
not is_free_flag and not is_for_sale_flag
|
||||
):
|
||||
# open website to subscribe
|
||||
props = row_right.operator(
|
||||
"wm.url_open",
|
||||
text="Subscribe to Full Plan",
|
||||
text="Requires Full Plan",
|
||||
icon_value=icon_value,
|
||||
)
|
||||
props.url = "https://www.blenderkit.com/plans/pricing/"
|
||||
@@ -324,9 +400,17 @@ def extension_draw_item_blenderkit(
|
||||
# Paid addons get a buy button and lead to their website link
|
||||
else:
|
||||
# Use the new modal operator
|
||||
base_price_value = bk_cache_pkg.get("user_price")
|
||||
if base_price_value in {None, "", "None"}:
|
||||
base_price_value = bk_cache_pkg.get("base_price")
|
||||
if base_price_value in {None, "", "None"}:
|
||||
buy_label = "Buy online"
|
||||
else:
|
||||
pad_str = get_perfect_price_padding(base_price_value)
|
||||
buy_label = f"Buy online {pad_str}${base_price_value}"
|
||||
props = row_right.operator(
|
||||
BK_OT_buy_extension_and_watch.bl_idname, # Use bl_idname
|
||||
text=f"Buy online ${bk_cache_pkg.get('base_price')}",
|
||||
text=buy_label,
|
||||
icon_value=icon_value,
|
||||
)
|
||||
props.url = bk_cache_pkg.get("website", "") # Pass URL
|
||||
@@ -528,12 +612,46 @@ def get_repository_by_url(url: str):
|
||||
return None
|
||||
|
||||
|
||||
def get_blenderkit_repository_cached():
|
||||
"""Get BlenderKit repository with time-based caching to avoid iterating repos per item."""
|
||||
global _cached_repository, _cached_repository_time
|
||||
now = time.time()
|
||||
if (
|
||||
now - _cached_repository_time < _REPO_LOOKUP_INTERVAL
|
||||
and _cached_repository is not None
|
||||
):
|
||||
# Verify the cached reference is still valid
|
||||
try:
|
||||
_ = _cached_repository.remote_url
|
||||
return _cached_repository
|
||||
except ReferenceError:
|
||||
pass
|
||||
_cached_repository = get_repository_by_url(EXTENSIONS_API_URL)
|
||||
_cached_repository_time = now
|
||||
return _cached_repository
|
||||
|
||||
|
||||
def clear_repo_cache():
|
||||
"""Clear the repository cache."""
|
||||
global _repo_cache_last_check, _repo_cache_last_result
|
||||
wm = bpy.context.window_manager
|
||||
cache_key = "blenderkit_extensions_repo_cache"
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
# Reset throttle so next ensure_repo_cache() does a fresh check
|
||||
_repo_cache_last_check = 0.0
|
||||
_repo_cache_last_result = False
|
||||
|
||||
|
||||
def _sanitize_pkg_for_cache(pkg):
|
||||
"""Keep values stored as string to prevent C overflow."""
|
||||
sanitized = {}
|
||||
for k, v in pkg.items():
|
||||
if isinstance(v, bool):
|
||||
sanitized[k] = v
|
||||
else:
|
||||
sanitized[k] = str(v)
|
||||
return sanitized
|
||||
|
||||
|
||||
def ensure_repo_cache():
|
||||
@@ -541,13 +659,28 @@ def ensure_repo_cache():
|
||||
Reads the .json file blender stores in \extensions\www_blenderkit_com\.blender_ext
|
||||
and parses it to a dict from json, we can use it then for drawing purposes and have the extra data BlenderKit api provides.
|
||||
Checks the modification time of the cache file and reloads it if necessary.
|
||||
|
||||
Uses a time-based throttle so filesystem checks happen at most once per
|
||||
_REPO_CACHE_CHECK_INTERVAL seconds, avoiding repeated stat calls when this
|
||||
function is called per-item during extension list drawing.
|
||||
"""
|
||||
global _repo_cache_last_check, _repo_cache_last_result
|
||||
|
||||
now = time.time()
|
||||
if now - _repo_cache_last_check < _REPO_CACHE_CHECK_INTERVAL:
|
||||
# Consume the reload signal so only the first caller in the throttle window
|
||||
# sees True — prevents the redraw timer from being registered on every draw tick.
|
||||
result = _repo_cache_last_result
|
||||
_repo_cache_last_result = False
|
||||
return result
|
||||
_repo_cache_last_check = now
|
||||
|
||||
reloaded_flag = False # Track if we actually reloaded
|
||||
wm = bpy.context.window_manager
|
||||
cache_key = "blenderkit_extensions_repo_cache"
|
||||
mtime_key = "blenderkit_extensions_repo_cache_mtime"
|
||||
|
||||
blenderkit_repository = get_repository_by_url(EXTENSIONS_API_URL)
|
||||
blenderkit_repository = get_blenderkit_repository_cached()
|
||||
if blenderkit_repository is None:
|
||||
# If repo doesn't exist, clear cache if it exists in window manager
|
||||
if cache_key in wm:
|
||||
@@ -566,7 +699,9 @@ def ensure_repo_cache():
|
||||
current_mtime = None
|
||||
try:
|
||||
if os.path.exists(cache_file):
|
||||
current_mtime = os.path.getmtime(cache_file)
|
||||
# Use int to avoid float precision loss when stored in Blender IDProperty
|
||||
# (IDProperties use single-precision floats, os.path.getmtime() returns double)
|
||||
current_mtime = int(os.path.getmtime(cache_file))
|
||||
except OSError as e: # Handle potential race condition or permission issue
|
||||
bk_logger.exception("Could not get modification time for %s.", cache_file)
|
||||
# Clear cache if we can't verify its freshness? Safer approach.
|
||||
@@ -621,15 +756,13 @@ def ensure_repo_cache():
|
||||
for pkg in data.get(
|
||||
"data", []
|
||||
): # Handle case where 'data' key might be missing
|
||||
if (
|
||||
isinstance(pkg, dict) and "id" in pkg
|
||||
): # Ensure pkg is a dict and 'id' key exists
|
||||
new_cache[pkg["id"][:32]] = pkg
|
||||
else:
|
||||
if not (isinstance(pkg, dict) and "id" in pkg):
|
||||
bk_logger.info("Skipping invalid package entry in cache: %s.", pkg)
|
||||
continue
|
||||
new_cache[pkg["id"][:32]] = _sanitize_pkg_for_cache(pkg)
|
||||
|
||||
wm[cache_key] = new_cache
|
||||
wm[mtime_key] = current_mtime # Update mtime only on successful load
|
||||
wm[mtime_key] = current_mtime # Stored as int to survive IDProperty round-trip
|
||||
|
||||
reloaded_flag = True # Mark that we reloaded successfully
|
||||
|
||||
@@ -652,9 +785,46 @@ def ensure_repo_cache():
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
|
||||
_repo_cache_last_result = reloaded_flag
|
||||
return reloaded_flag # Return whether cache was actually reloaded
|
||||
|
||||
|
||||
def update_cache_with_asset_prices(assets):
|
||||
"""Copy addon pricing info from search assets into the extensions cache."""
|
||||
if not assets:
|
||||
return
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
cache_key = "blenderkit_extensions_repo_cache"
|
||||
if cache_key not in wm:
|
||||
wm[cache_key] = {}
|
||||
|
||||
cache = wm[cache_key]
|
||||
for asset in assets:
|
||||
if not isinstance(asset, dict):
|
||||
continue
|
||||
if asset.get("assetType") != "addon":
|
||||
continue
|
||||
dict_params = asset.get("dictParameters") or {}
|
||||
extension_id = dict_params.get("extensionId") or asset.get("extensionId")
|
||||
if not extension_id:
|
||||
continue
|
||||
|
||||
cache_key_entry = extension_id[:32]
|
||||
cache_entry = cache.get(cache_key_entry)
|
||||
if cache_entry is None:
|
||||
cache_entry = {}
|
||||
cache[cache_key_entry] = cache_entry
|
||||
|
||||
base_price = asset.get("basePrice")
|
||||
if base_price not in {None, "", "None"}:
|
||||
cache_entry["base_price"] = str(base_price)
|
||||
|
||||
user_price = asset.get("userPrice")
|
||||
if user_price not in {None, "", "None"}:
|
||||
cache_entry["user_price"] = str(user_price)
|
||||
|
||||
|
||||
def ensure_repo_order():
|
||||
"""Ensure order of repositories in Blender's preferences."""
|
||||
# get the blenderkit repository
|
||||
|
||||
Reference in New Issue
Block a user