Files
blender-portable-repo/extensions/user_default/blenderkit/override_extension_draw.py
T
Raincloud 692e200ffe work
save startup blend for animation tab & whatnot
2026-04-08 12:10:18 -06:00

929 lines
34 KiB
Python

"""
This is a separate library that overrides the extension_draw_item method from Blender extensions list display.
The original code is in the bl_extension_ui.py file in the Blender source code.
The override library can be placed in multiple addons, and the override should happen only once.
The override is done by replacing the original method with the new one, and backing up the original method.
The original method is then called from the new method, with the same arguments, but with the new code added.
"""
import json
import os
import time
import re
import logging
from . import icons, version_compare
import blf
import bpy
import bl_pkg.bl_extension_ui as exui
from bpy.props import IntProperty, StringProperty
from bpy.types import Operator
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):
"""Opens URL to buy extension and starts a modal timer to refresh repo periodically."""
bl_idname = "bk.buy_extension_and_watch"
bl_label = "Buy Extension Online and Watch"
bl_options = {"REGISTER", "UNDO"}
url: StringProperty(
name="URL",
description="Website URL to open",
)
repo_index: IntProperty(
name="Repository Index",
description="Index of the repository to refresh",
default=-1,
)
_timer = None
_last_refresh_time = 0
_start_time = 0
_refresh_interval = 60 # seconds
_max_duration = 300 # seconds (5 minutes timeout)
def execute(self, context):
if not self.url:
self.report({"ERROR"}, "No URL specified.")
return {"CANCELLED"}
if self.repo_index == -1:
self.report({"ERROR"}, "No repository index specified.")
return {"CANCELLED"}
# Open the URL
try:
bpy.ops.wm.url_open(url=self.url)
bk_logger.info("Opening buy URL: %s.", self.url)
except Exception as e:
self.report({"ERROR"}, f"Could not open URL: {e}")
# Don't cancel, maybe the user still wants the refresh?
# Decide if you want modal to continue even if URL fails
# Add modal handler and timer
wm = context.window_manager
self._timer = wm.event_timer_add(
1.0, window=context.window
) # Check every second
wm.modal_handler_add(self)
self._start_time = time.time()
self._last_refresh_time = (
self._start_time
) # Initialize to avoid immediate refresh
bk_logger.info(
"Started watching repository index %s for updates.", self.repo_index
)
if context and context.area:
context.area.tag_redraw() # Update UI to show operator is running if needed
return {"RUNNING_MODAL"}
def modal(self, context, event):
current_time = time.time()
# --- Exit Conditions ---
# 1. User closed Preferences or changed area
if context.area is None or context.area.type != "PREFERENCES":
bk_logger.info("Preferences window closed or changed, stopping watcher.")
self.cancel(context)
return {"CANCELLED"}
# 2. Timeout
if current_time - self._start_time > self._max_duration:
bk_logger.info("Watcher timed out, stopping.")
self.cancel(context)
return {"CANCELLED"}
# 3. User cancellation
if event.type in {"RIGHTMOUSE", "ESC"}:
bk_logger.info("Watcher cancelled by user.")
self.cancel(context)
return {"CANCELLED"}
# --- Timer Logic ---
if event.type == "TIMER":
# Check if refresh interval has passed
if current_time - self._last_refresh_time >= self._refresh_interval:
bk_logger.info(
"Refresh interval reached, attempting sync for repo index %s...",
self.repo_index,
)
try:
# Check if repo still exists at that index
if self.repo_index < len(context.preferences.extensions.repos):
bpy.ops.extensions.repo_sync(repo_index=self.repo_index)
bk_logger.info(
"repo_sync called for index %s.", self.repo_index
)
else:
bk_logger.info(
"Repository index %s no longer valid.", self.repo_index
)
# Optionally cancel here if repo is gone
except:
# This might fail if another operation is in progress
bk_logger.exception("extensions.repo_sync failed.")
finally:
self._last_refresh_time = (
current_time # Reset timer regardless of success
)
return {"PASS_THROUGH"} # Pass other events through
def cancel(self, context):
if self._timer:
wm = context.window_manager
wm.event_timer_remove(self._timer)
self._timer = None
bk_logger.info("Watcher timer removed.")
if context and context.area:
context.area.tag_redraw() # Update UI
# --- End New Modal Operator ---
def redraw_preferences_once():
"""Tag the redraw on the Blender preferences.
Meant to be registered as a timer, runs just once.
"""
for window in bpy.context.window_manager.windows:
screen = window.screen
if not screen:
continue
for area in screen.areas:
if area.type != "PREFERENCES":
continue
for region in area.regions:
if region.type in {"UI", "WINDOW"}:
region.tag_redraw()
return None
def extension_draw_item_blenderkit(
layout,
*,
pkg_id, # `str`
item_local, # `PkgManifest_Normalized | None`
item_remote, # `PkgManifest_Normalized | None`
is_enabled, # `bool`
is_outdated, # `bool`
show, # `bool`.
mark, # `bool | None`.
# General vars.
repo_index, # `int`
repo_item, # `RepoItem`
operation_in_progress, # `bool`
extensions_warnings, # `dict[str, list[str]]`
show_developer_ui, # `bool`
):
### BlenderKit cache code
# Ensure cache is up-to-date before drawing
cache_reloaded = ensure_repo_cache()
if cache_reloaded:
# If cache was just reloaded, tag UI for redraw
# as UILayout doesn't have tag_redraw we call a custom function
if bpy.app.timers.is_registered(redraw_preferences_once):
bpy.app.timers.unregister(redraw_preferences_once)
bpy.app.timers.register(redraw_preferences_once, first_interval=0.01)
bk_logger.info("Cache reloaded, tagging preferences for redraw.")
# check if the cache is already in the window manager
if "blenderkit_extensions_repo_cache" not in bpy.context.window_manager:
# Log if cache is missing after trying to ensure it
bk_logger.info(
"Extension cache not available in window_manager after ensure_repo_cache call."
)
# Optionally draw a minimal representation or return early to avoid errors
# For now, just return to avoid potential errors accessing bk_ext_cache
return
bk_ext_cache = bpy.context.window_manager["blenderkit_extensions_repo_cache"]
bk_cache_pkg = bk_ext_cache.get(pkg_id[:32], None)
### end of BlenderKit cache code
item = item_local or item_remote
is_installed = item_local is not None
has_remote = repo_item.remote_url != ""
if item_remote is not None:
pkg_block = item_remote.block
else:
pkg_block = None
if is_enabled:
item_warnings = extensions_warnings.get(
exui.pkg_repo_module_prefix(repo_item) + pkg_id, []
)
else:
item_warnings = []
# Left align so the operator text isn't centered.
colsub = layout.column()
row = colsub.row(align=True)
if show:
props = row.operator(
"extensions.package_show_clear", text="", icon="DOWNARROW_HLT", emboss=False
)
else:
props = row.operator(
"extensions.package_show_set", text="", icon="RIGHTARROW", emboss=False
)
props.pkg_id = pkg_id
props.repo_index = repo_index
if mark is not None:
if mark:
props = row.operator(
"extensions.package_mark_clear",
text="",
icon="RADIOBUT_ON",
emboss=False,
)
else:
props = row.operator(
"extensions.package_mark_set",
text="",
icon="RADIOBUT_OFF",
emboss=False,
)
props.pkg_id = pkg_id
props.repo_index = repo_index
sub = row.row()
sub.active = is_enabled
# Without checking `is_enabled` here, there is no way for the user to know if an extension
# is enabled or not, which is useful to show - when they may be considering removing/updating
# extensions based on them being used or not.
if pkg_block or item_warnings:
sub.label(text=item.name, icon="ERROR", translate=False)
else:
sub.label(text=item.name, translate=False)
# Add a top-level row so `row_right` can have a grayed out button/label
# without graying out the menu item since# that is functional.
row_right_toplevel = row.row(align=True)
if operation_in_progress:
row_right_toplevel.enabled = False
row_right_toplevel.alignment = "RIGHT"
row_right = row_right_toplevel.row()
row_right.alignment = "RIGHT"
if has_remote and (item_remote is not None):
if pkg_block is not None:
row_right.label(text="Blocked ")
elif is_installed:
# 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
# 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 can_download_value is True:
# if the addon is also for sale, it means the user purchased it and we write "install purchased"
if is_for_sale_flag:
props = row_right.operator(
"extensions.package_install",
text="Install purchased",
icon_value=icon_value,
)
else:
props = row_right.operator(
"extensions.package_install",
text="Install",
icon_value=icon_value,
)
props.repo_index = repo_index
props.pkg_id = pkg_id
# 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
):
props = row_right.operator(
"wm.url_open",
text="Requires Full Plan",
icon_value=icon_value,
)
props.url = "https://www.blenderkit.com/plans/pricing/"
# 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=buy_label,
icon_value=icon_value,
)
props.url = bk_cache_pkg.get("website", "") # Pass URL
props.repo_index = repo_index # Pass repo index
### end of BlenderKit specific code
else:
# Right space for alignment with the button.
if has_remote and (item_remote is None):
# There is a local item with no remote
row_right.label(text="Orphan ")
row_right.active = False
row_right = row_right_toplevel.row(align=True)
row_right.alignment = "RIGHT"
row_right.separator()
# NOTE: Keep space between any buttons and this menu to prevent stray clicks accidentally running install.
# The separator is around together with the align to give some space while keeping the button and the menu
# still close-by. Used `extension_path` so the menu can access "this" extension.
row_right.context_string_set(
"extension_path", "{:s}.{:s}".format(repo_item.module, pkg_id)
)
row_right.menu("USERPREF_MT_extensions_item", text="", icon="DOWNARROW_HLT")
if show:
import os
from bpy.app.translations import pgettext_iface as iface_
col = layout.column()
row = col.row()
row.active = is_enabled
# The full tagline may be multiple lines (not yet supported by Blender's UI).
row.label(text=" {:s}.".format(item.tagline), translate=False)
col.separator(type="LINE")
col_info = layout.column()
col_info.active = is_enabled
split = col_info.split(factor=0.15)
col_a = split.column()
col_b = split.column()
col_a.alignment = "RIGHT"
if pkg_block is not None:
col_a.label(text="Blocked")
col_b.label(text=pkg_block.reason, translate=False)
if item_warnings:
col_a.label(text="Warning")
col_b.label(text=item_warnings[0])
if len(item_warnings) > 1:
for value in item_warnings[1:]:
col_a.label(text="")
col_b.label(text=value)
# pylint: disable-next=undefined-loop-variable
if value := (item_remote or item_local).website:
col_a.label(text="Website")
col_b.split(factor=0.5).operator(
"wm.url_open",
text=exui.domain_extract_from_url(value),
icon="URL",
).url = value
del value
if item.type == "add-on":
col_a.label(text="Permissions")
# WARNING: while this is documented to be a dict, old packages may contain a list of strings.
# As it happens dictionary keys & list values both iterate over string,
# however we will want to show the dictionary values eventually.
if value := item.permissions:
col_b.label(
text=", ".join([iface_(x).title() for x in value]), translate=False
)
else:
col_b.label(text="No permissions specified")
del value
col_a.label(text="Maintainer")
col_b.label(text=item.maintainer, translate=False)
col_a.label(text="Version")
if is_outdated:
col_b.label(
text=iface_("{:s} ({:s} available)").format(
item.version, item_remote.version
),
translate=False,
)
else:
col_b.label(text=item.version, translate=False)
if has_remote and (item_remote is not None):
col_a.label(text="Size")
col_b.label(
text=exui.size_as_fmt_string(item_remote.archive_size), translate=False
)
col_a.label(text="License")
col_b.label(text=item.license, translate=False)
col_a.label(text="Repository")
col_b.label(text=repo_item.name, translate=False)
if is_installed:
col_a.label(text="Path")
col_b.label(text=os.path.join(repo_item.directory, pkg_id), translate=False)
def extension_draw_item_override(
layout,
*,
pkg_id, # `str`
item_local, # `PkgManifest_Normalized | None`
item_remote, # `PkgManifest_Normalized | None`
is_enabled, # `bool`
is_outdated, # `bool`
show, # `bool`.
mark, # `bool | None`.
# General vars.
repo_index, # `int`
repo_item, # `RepoItem`
operation_in_progress, # `bool`
extensions_warnings, # `dict[str, list[str]]`
show_developer_ui=False, # `bool`
):
# filter by verification state, only for blenderkit repository
if repo_item.remote_url == EXTENSIONS_API_URL:
extension_draw_item_blenderkit(
layout,
pkg_id=pkg_id,
item_local=item_local,
item_remote=item_remote,
is_enabled=is_enabled,
is_outdated=is_outdated,
show=show,
mark=mark,
repo_index=repo_index,
repo_item=repo_item,
operation_in_progress=operation_in_progress,
extensions_warnings=extensions_warnings,
show_developer_ui=show_developer_ui,
)
return True
# show developer ui only needs to be passed since blender 4.4
if bpy.app.version >= (4, 4):
exui.extension_draw_item_original(
layout,
pkg_id=pkg_id,
item_local=item_local,
item_remote=item_remote,
is_enabled=is_enabled,
is_outdated=is_outdated,
show=show,
mark=mark,
repo_index=repo_index,
repo_item=repo_item,
operation_in_progress=operation_in_progress,
extensions_warnings=extensions_warnings,
show_developer_ui=show_developer_ui,
)
else:
exui.extension_draw_item_original(
layout,
pkg_id=pkg_id,
item_local=item_local,
item_remote=item_remote,
is_enabled=is_enabled,
is_outdated=is_outdated,
show=show,
mark=mark,
repo_index=repo_index,
repo_item=repo_item,
operation_in_progress=operation_in_progress,
extensions_warnings=extensions_warnings,
)
return True
def override_draw_function():
if hasattr(exui, "extension_draw_item_original"):
return False
exui.extension_draw_item_original = exui.extension_draw_item
exui.extension_draw_item = extension_draw_item_override
return True
def get_repository_by_url(url: str):
"""Get the repository by its remote URL, from registered blenderkit Extension repositories."""
for r in bpy.context.preferences.extensions.repos:
if r.remote_url == url:
return r
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():
r"""
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_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:
del wm[cache_key]
bk_logger.info("Cleared stale extension cache for missing repository.")
if mtime_key in wm:
del wm[mtime_key]
bk_logger.debug("Repository not found, exiting check.")
return False # No repo, nothing loaded
# get the path to the cache file which is in repository directory under /.blender_ext/index.json
cache_file = os.path.join(
blenderkit_repository.directory, ".blender_ext", "index.json"
)
current_mtime = None
try:
if os.path.exists(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.
if cache_key in wm:
del wm[cache_key]
bk_logger.info("Cleared extension cache due to mtime access error.")
if mtime_key in wm:
del wm[mtime_key]
return False # Error, nothing loaded
stored_mtime = wm.get(mtime_key, None)
# --- Determine if reload is needed ---
should_reload = False
if cache_key not in wm:
if current_mtime is not None: # Only load if file actually exists
should_reload = True # Cache doesn't exist, need initial load.
else:
# Cache doesn't exist and file doesn't exist/accessible. Fall through to check if we need to clear.
pass
elif current_mtime is None:
# Cache exists in wm, but file is gone/inaccessible. Clear stale cache.
del wm[cache_key]
if mtime_key in wm:
del wm[mtime_key]
return False # Cleared stale cache, did not load new data
elif cache_key not in wm and current_mtime is None:
# Cache doesn't exist, and file doesn't exist. Nothing to do or load.
return False
elif (
cache_key in wm and (stored_mtime is None or stored_mtime != current_mtime)
) or (
cache_key not in wm and current_mtime is not None
): # Reload if cache exists and is outdated, OR if cache doesn't exist but file does
should_reload = True # Cache exists but is outdated or missing mtime.
if not should_reload:
# Cache exists and is up-to-date
return False # Nothing reloaded
# --- (Re)Load cache ---
try:
with open(cache_file, "r", encoding="utf-8") as f: # Specify encoding
data_str = f.read()
data = json.loads(data_str)
# store the data as a dict with keys being the package names (first 32 chars)
new_cache = {}
for pkg in data.get(
"data", []
): # Handle case where 'data' key might be missing
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 # Stored as int to survive IDProperty round-trip
reloaded_flag = True # Mark that we reloaded successfully
except json.JSONDecodeError:
bk_logger.warning(
"Error decoding JSON from %s. Cache not loaded/updated.", cache_file
)
# Clear potentially corrupt cache? Or leave old one? Clearing is safer.
if cache_key in wm:
del wm[cache_key]
bk_logger.info("Cleared cache due to JSON error.")
if mtime_key in wm:
del wm[mtime_key]
except Exception:
bk_logger.exception("Error reading or processing cache file %s.", cache_file)
# Clear potentially corrupt cache?
if cache_key in wm:
del wm[cache_key]
bk_logger.info("Cleared cache due to file processing error.")
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
blenderkit_repository = get_repository_by_url(EXTENSIONS_API_URL)
if blenderkit_repository is None:
return
# get all repositories
all_repos = bpy.context.preferences.extensions.repos
# get all online repositories except blenderkit
online_repos = [] # need to convert repos to dicts
remove_online_repos = []
for r in all_repos:
if r.remote_url != EXTENSIONS_API_URL and r.remote_url != "":
repo_dict = {
"name": r.name,
"module": r.module,
"use_remote_url": r.use_remote_url,
"remote_url": r.remote_url,
"use_sync_on_startup": r.use_sync_on_startup,
"use_cache": r.use_cache,
"use_access_token": r.use_access_token,
"access_token": r.access_token,
"use_custom_directory": r.use_custom_directory,
"custom_directory": r.custom_directory,
"enabled": r.enabled,
}
online_repos.append(repo_dict)
remove_online_repos.append(r)
# remove all online repositories except blenderkit
for r in remove_online_repos:
all_repos.remove(r)
# add all other repositories back
for r in online_repos:
# complete list of properties of a repository:
#'access_token', 'custom_directory', 'directory', 'enabled', 'module', 'name', 'remote_url', 'rna_type', 'source', 'use_access_token', 'use_cache', 'use_custom_directory', 'use_remote_url', 'use_sync_on_startup'
new_repo = all_repos.new()
new_repo.name = r["name"]
new_repo.module = r["module"]
new_repo.use_remote_url = r["use_remote_url"]
new_repo.remote_url = r["remote_url"]
new_repo.use_sync_on_startup = r["use_sync_on_startup"]
new_repo.use_cache = r["use_cache"]
new_repo.use_access_token = r["use_access_token"]
new_repo.access_token = r["access_token"]
new_repo.use_custom_directory = r["use_custom_directory"]
new_repo.custom_directory = r["custom_directory"]
new_repo.enabled = r["enabled"]
def ensure_repository(api_key: str = ""):
"""Ensure that the blenderkit extensions repository is correctly added in Blender's preferences.
If the repository is not present, it is added. If the repository is present, but the API key is not set, it is set.
"""
blenderkit_repository = get_repository_by_url(EXTENSIONS_API_URL)
if blenderkit_repository is None:
blenderkit_repository = bpy.context.preferences.extensions.repos.new()
blenderkit_repository.name = "www.blenderkit.com"
blenderkit_repository.module = "www_blenderkit_com"
blenderkit_repository.use_remote_url = True
blenderkit_repository.remote_url = EXTENSIONS_API_URL
blenderkit_repository.use_sync_on_startup = True
if api_key != "":
blenderkit_repository.use_access_token = True
blenderkit_repository.access_token = api_key
else:
# let's try to import blenderkit preferences and get the api key
# try:
user_preferences = bpy.context.preferences.addons[__package__].preferences
api_key = user_preferences.api_key
if api_key != "":
blenderkit_repository.use_access_token = True
blenderkit_repository.access_token = api_key
else:
# clear after logout
blenderkit_repository.use_access_token = False
blenderkit_repository.access_token = ""
# pass
# ensure_repo_order()
ensure_repo_cache()
def register():
ensure_repository()
override_draw_function()
bpy.utils.register_class(BK_OT_buy_extension_and_watch) # Register new operator
def unregister():
exui.extension_draw_item = exui.extension_draw_item_original
del exui.extension_draw_item_original
bpy.utils.unregister_class(BK_OT_buy_extension_and_watch) # Unregister new operator