2026-02-16
This commit is contained in:
+35
-2
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "BlenderKit Online Asset Library",
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
|
||||
"version": (3, 17, 0, 251008), # X.Y.Z.yymmdd
|
||||
"version": (3, 18, 0, 251121), # X.Y.Z.yymmdd
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Properties > BlenderKit",
|
||||
"description": "Boost your workflow with drag&drop assets from the community driven library.",
|
||||
@@ -28,7 +28,7 @@ bl_info = {
|
||||
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
VERSION = (3, 17, 0, 251008)
|
||||
VERSION = (3, 18, 0, 251121)
|
||||
|
||||
import logging
|
||||
import random
|
||||
@@ -297,6 +297,11 @@ def asset_type_callback(self, context):
|
||||
6,
|
||||
),
|
||||
]
|
||||
|
||||
# Only add addon option for Blender 4.2+
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-ons", "Find add-ons", "PLUGIN", 7))
|
||||
else:
|
||||
items = [
|
||||
("MODEL", "Model", "Upload a model", "OBJECT_DATAMODE", 0),
|
||||
@@ -314,6 +319,11 @@ def asset_type_callback(self, context):
|
||||
),
|
||||
]
|
||||
|
||||
# Only add addon option for Blender 4.2+
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-on", "Upload an addon", "PLUGIN", 7))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@@ -1164,6 +1174,19 @@ class BlenderKitGeoToolSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
pass
|
||||
|
||||
|
||||
class BlenderKitAddonSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
search_installed: BoolProperty(
|
||||
name="Installed Only",
|
||||
description="Show only addons that are already installed in Blender",
|
||||
default=False,
|
||||
update=lambda self, context: (
|
||||
search.refresh_search()
|
||||
if context.window_manager.blenderkitUI.asset_type == "ADDON"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
texture_resolution_max: IntProperty(
|
||||
name="Texture Resolution Max",
|
||||
@@ -2401,6 +2424,12 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
update=search.search_update,
|
||||
) # In future we can subsets like sexualized, pornography or violence subset. And allow users choose what is part of NSFW.
|
||||
|
||||
temp_enabled_addons: StringProperty(
|
||||
name="Temporarily Enabled Addons",
|
||||
description="JSON string of temporarily enabled addon package IDs that should be disabled on next session",
|
||||
default="[]",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if self.api_key.strip() == "":
|
||||
@@ -2507,6 +2536,7 @@ classes = (
|
||||
BlenderKitBrushUploadProps,
|
||||
BlenderKitGeoToolSearchProps,
|
||||
BlenderKitNodeGroulUploadProps,
|
||||
BlenderKitAddonSearchProps,
|
||||
)
|
||||
|
||||
|
||||
@@ -2573,6 +2603,9 @@ def register():
|
||||
bpy.types.NodeTree.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
)
|
||||
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
|
||||
type=BlenderKitAddonSearchProps
|
||||
)
|
||||
if bpy.app.factory_startup is False:
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
global_vars.PREFS = utils.get_preferences_as_dict()
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
include:
|
||||
- "**/*.py"
|
||||
|
||||
exclude_dirs:
|
||||
- "tests"
|
||||
- ".venv"
|
||||
- "__pycache__"
|
||||
|
||||
skips:
|
||||
- "B404" # https://bandit.readthedocs.io/en/1.7.10/blacklists/blacklist_imports.html#b404-import-subprocess
|
||||
- "B603" # https://bandit.readthedocs.io/en/1.7.10/plugins/b603_subprocess_without_shell_equals_true.html
|
||||
- "B608" # https://bandit.readthedocs.io/en/1.7.10/plugins/b608_hardcoded_sql_expressions.html
|
||||
+17
@@ -101,6 +101,7 @@ def append_nodegroup(
|
||||
nodegroup.use_fake_user = fake_user
|
||||
|
||||
# Create target object automatically for geometry nodegroups when no target is provided
|
||||
auto_created_target: Optional[bpy.types.Object] = None
|
||||
if nodegroup.bl_rna.identifier == "GeometryNodeTree" and not target_object:
|
||||
# Create a default mesh cube
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
@@ -109,6 +110,7 @@ def append_nodegroup(
|
||||
target_obj = bpy.context.active_object
|
||||
target_obj.name = "GeometryNodeTarget"
|
||||
target_object = target_obj.name
|
||||
auto_created_target = target_obj
|
||||
|
||||
# Make sure it's selected and active
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
@@ -245,6 +247,21 @@ def append_nodegroup(
|
||||
added_to_editor = True
|
||||
break
|
||||
|
||||
# Ensure automatically created targets receive the nodegroup as modifier
|
||||
if auto_created_target:
|
||||
gn_mod = None
|
||||
for mod in auto_created_target.modifiers:
|
||||
if mod.type == "NODES":
|
||||
gn_mod = mod
|
||||
break
|
||||
if not gn_mod:
|
||||
gn_mod = auto_created_target.modifiers.new(
|
||||
name=nodegroup.name, type="NODES"
|
||||
)
|
||||
gn_mod.node_group = nodegroup
|
||||
auto_created_target.select_set(True)
|
||||
bpy.context.view_layer.objects.active = auto_created_target
|
||||
|
||||
return nodegroup, added_to_editor
|
||||
|
||||
|
||||
|
||||
+393
-298
File diff suppressed because it is too large
Load Diff
+989
-773
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -82,7 +82,7 @@ def get_texture_ui(tpath, iname):
|
||||
|
||||
|
||||
def check_thumbnail(props, imgpath):
|
||||
# TODO implement check if the file exists, if size is corect etc. needs some care
|
||||
# TODO implement check if the file exists, if size is correct etc. needs some care
|
||||
if imgpath == "":
|
||||
props.has_thumbnail = False
|
||||
return None
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ if __name__ == "__main__":
|
||||
asset_data["files"][0]["file_name"] = file_name
|
||||
if not has_url:
|
||||
bg_blender.progress(
|
||||
"couldn't download asset for thumnbail re-rendering"
|
||||
"couldn't download asset for thumbnail re-rendering"
|
||||
)
|
||||
exit()
|
||||
# download first, or rather make sure if it's already downloaded
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ if __name__ == "__main__":
|
||||
asset_data["files"][0]["file_name"] = file_name
|
||||
if has_url is not True:
|
||||
bg_blender.progress(
|
||||
"couldn't download asset for thumnbail re-rendering"
|
||||
"couldn't download asset for thumbnail re-rendering"
|
||||
)
|
||||
bg_blender.progress("downloading asset")
|
||||
fpath = bg_utils.download_asset_file(
|
||||
|
||||
+5
-6
@@ -31,6 +31,8 @@ from bpy.props import BoolProperty
|
||||
|
||||
from . import client_lib, client_tasks, datas, global_vars, reports, tasks_queue, utils
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
|
||||
CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F"
|
||||
REFRESH_RESERVE = 60 * 60 * 24 * 3 # 3 days
|
||||
@@ -103,10 +105,9 @@ def clean_login_data():
|
||||
preferences.api_key_timeout = 0
|
||||
global_vars.BKIT_PROFILE = datas.MineProfile()
|
||||
# Cleanup also the api key in the extensions repository setting and clean the cache
|
||||
from . import override_extension_draw
|
||||
|
||||
override_extension_draw.ensure_repository(api_key="")
|
||||
override_extension_draw.clear_repo_cache()
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
override_extension_draw.ensure_repository(api_key="")
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
|
||||
def logout() -> None:
|
||||
@@ -158,8 +159,6 @@ def write_tokens(auth_token, refresh_token, oauth_response):
|
||||
preferences.api_key = auth_token # triggers api_key update function
|
||||
# write token also to extensions repository setting and clear the cache
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
|
||||
override_extension_draw.ensure_repository(api_key=auth_token)
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
|
||||
+5
-5
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"last_check": "2025-12-01 11:02:25.858363",
|
||||
"backup_date": "October-27-2025",
|
||||
"last_check": "2026-01-12 10:24:10.400844",
|
||||
"backup_date": "December-1-2025",
|
||||
"update_ready": true,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
"just_updated": false,
|
||||
"version_text": {
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.0.251121/blenderkit-v3.18.0.251121.zip",
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.1.251219/blenderkit-v3.18.1.251219.zip",
|
||||
"version": [
|
||||
3,
|
||||
18,
|
||||
0,
|
||||
251121
|
||||
1,
|
||||
251219
|
||||
]
|
||||
}
|
||||
}
|
||||
+27
-20
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
@@ -6,9 +8,14 @@ import gpu
|
||||
|
||||
from .. import image_utils, ui_bgl
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
from .bl_ui_image import BL_UI_Image
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_Button(BL_UI_Widget):
|
||||
"""Image Button for assets in asset bar."""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
super().__init__(x, y, width, height)
|
||||
self._text_color = (1.0, 1.0, 1.0, 1.0)
|
||||
@@ -89,7 +96,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
except Exception as e:
|
||||
self.__image = None
|
||||
|
||||
def set_image_colorspace(self, colorspace):
|
||||
def set_image_colorspace(self, colorspace: str = ""):
|
||||
image_utils.set_colorspace(self.__image, colorspace)
|
||||
|
||||
def set_image(self, rel_filepath):
|
||||
@@ -98,22 +105,10 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
try:
|
||||
if self.__image is None or self.__image.filepath != rel_filepath:
|
||||
imgname = f".{os.path.basename(rel_filepath)}"
|
||||
img = bpy.data.images.get(imgname)
|
||||
if img is not None:
|
||||
self.__image = img
|
||||
else:
|
||||
self.__image = bpy.data.images.load(
|
||||
rel_filepath, check_existing=True
|
||||
)
|
||||
self.__image.name = imgname
|
||||
self.__image = image_utils.IMG(name=imgname, filepath=rel_filepath)
|
||||
|
||||
self.__image.gl_load()
|
||||
|
||||
if self.__image and len(self.__image.pixels) == 0:
|
||||
self.__image.reload()
|
||||
self.__image.gl_load()
|
||||
except Exception as e:
|
||||
print(f"BL_UI_BUTTON set_image() error: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON set_image() error:")
|
||||
self.__image = None
|
||||
|
||||
def get_image_path(self):
|
||||
@@ -185,7 +180,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
y_screen_flip = self.get_area_height() - self.y_screen
|
||||
off_x, off_y = self.__image_position
|
||||
sx, sy = self.__image_size
|
||||
ui_bgl.draw_image(
|
||||
ui_bgl.draw_image_runtime(
|
||||
self.x_screen + off_x,
|
||||
y_screen_flip - off_y - sy,
|
||||
sx,
|
||||
@@ -206,10 +201,22 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
self.__state = 1
|
||||
try:
|
||||
self.mouse_down_func(self)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down() error:")
|
||||
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_mouse_down_right(self, mouse_down_right_func):
|
||||
self.mouse_down_right_func = mouse_down_right_func
|
||||
|
||||
def mouse_down_right(self, x, y):
|
||||
if self.is_in_rect(x, y):
|
||||
try:
|
||||
self.mouse_down_right_func(self)
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+9
-5
@@ -1,7 +1,11 @@
|
||||
import traceback
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_OT_draw_operator(Operator):
|
||||
bl_idname = "object.bl_ui_ot_draw_operator"
|
||||
@@ -23,7 +27,7 @@ class BL_UI_OT_draw_operator(Operator):
|
||||
for widget in self.widgets:
|
||||
widget.init(context)
|
||||
|
||||
def on_invoke(self, context, event):
|
||||
def on_invoke(self, context, event) -> Optional[bool]:
|
||||
pass
|
||||
|
||||
def on_finish(self, context):
|
||||
@@ -105,7 +109,7 @@ class BL_UI_OT_draw_operator(Operator):
|
||||
|
||||
|
||||
def draw_callback_px_separated(self, op, context):
|
||||
# separated only for puprpose of profiling
|
||||
# separated only for purpose of profiling
|
||||
try:
|
||||
# hide during animation playback, to improve performance
|
||||
if context.screen.is_animation_playing:
|
||||
@@ -113,5 +117,5 @@ def draw_callback_px_separated(self, op, context):
|
||||
if context.area.as_pointer() == self.active_area_pointer:
|
||||
for widget in self.widgets:
|
||||
widget.draw()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
bk_logger.exception("Error in draw_callback_px_separated: ")
|
||||
|
||||
+15
-19
@@ -1,12 +1,21 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from .. import image_utils, ui_bgl
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BL_UI_Image(BL_UI_Widget):
|
||||
"""A simple image widget.
|
||||
|
||||
Used to display bigger thumbnail with additional info,
|
||||
while hover over a button.
|
||||
"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
super().__init__(x, y, width, height)
|
||||
|
||||
@@ -36,25 +45,12 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
try:
|
||||
if self.__image is None or self.__image.filepath != rel_filepath:
|
||||
imgname = f".{os.path.basename(rel_filepath)}"
|
||||
img = bpy.data.images.get(imgname)
|
||||
if img is not None:
|
||||
self.__image = img
|
||||
else:
|
||||
self.__image = bpy.data.images.load(
|
||||
rel_filepath, check_existing=True
|
||||
)
|
||||
self.__image.name = imgname
|
||||
|
||||
self.__image.gl_load()
|
||||
|
||||
if self.__image and len(self.__image.pixels) == 0:
|
||||
self.__image.reload()
|
||||
self.__image.gl_load()
|
||||
self.__image = image_utils.IMG(name=imgname, filepath=rel_filepath)
|
||||
except Exception as e:
|
||||
print(f"BL_UI_BUTTON: exception in set_image(): {e}")
|
||||
bk_logger.exception("BL_UI_BUTTON: exception in set_image(): %s", e)
|
||||
self.__image = None
|
||||
|
||||
def set_image_colorspace(self, colorspace):
|
||||
def set_image_colorspace(self, colorspace: str = ""):
|
||||
image_utils.set_colorspace(self.__image, colorspace)
|
||||
|
||||
def get_image_path(self):
|
||||
@@ -69,9 +65,9 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
def draw(self):
|
||||
if not self._is_visible:
|
||||
return
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.shader.bind()
|
||||
|
||||
self.batch_panel.draw(self.shader)
|
||||
|
||||
self.draw_image()
|
||||
@@ -81,7 +77,7 @@ class BL_UI_Image(BL_UI_Widget):
|
||||
y_screen_flip = self.get_area_height() - self.y_screen
|
||||
off_x, off_y = self.__image_position
|
||||
sx, sy = self.__image_size
|
||||
ui_bgl.draw_image(
|
||||
ui_bgl.draw_image_runtime(
|
||||
self.x_screen + off_x,
|
||||
y_screen_flip - off_y - sy,
|
||||
sx,
|
||||
|
||||
+9
-7
@@ -20,6 +20,11 @@ class BL_UI_Widget:
|
||||
self._is_visible = True
|
||||
self._is_active = True # if the widget needs to be disabled
|
||||
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
self.shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def set_location(self, x, y):
|
||||
# if self.x != x or self.y != y or self.x_screen != x or self.y_screen != y:
|
||||
# bpy.context.region.tag_redraw()
|
||||
@@ -71,6 +76,8 @@ class BL_UI_Widget:
|
||||
if not self._is_visible:
|
||||
return
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", self._bg_color)
|
||||
|
||||
@@ -97,11 +104,6 @@ class BL_UI_Widget:
|
||||
(self.x_screen + self.width, y_screen_flip),
|
||||
)
|
||||
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
self.shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
self.batch_panel = batch_for_shader(
|
||||
self.shader, "TRIS", {"pos": vertices}, indices=indices
|
||||
)
|
||||
@@ -187,8 +189,8 @@ class BL_UI_Widget:
|
||||
):
|
||||
# print('is in rect!?')
|
||||
# print('area height', area_height)
|
||||
# print ('x sceen ',self.x_screen,'x ', x, 'width', self.width)
|
||||
# print ('widghet y', widget_y,'y', y, 'height',self.height)
|
||||
# print ('x screen ',self.x_screen,'x ', x, 'width', self.width)
|
||||
# print ('widget y', widget_y,'y', y, 'height',self.height)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "blenderkit"
|
||||
type = "add-on"
|
||||
version = "3.17.0-251008" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
version = "3.18.0-251121" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
|
||||
name = "BlenderKit Online Asset Library"
|
||||
tagline = "Drag & drop of assets from the community driven library"
|
||||
|
||||
+1
@@ -124,6 +124,7 @@ def handle_categories_task(task: client_tasks.Task):
|
||||
"BRUSH": ["brush"],
|
||||
"NODEGROUP": ["nodegroup"],
|
||||
"PRINTABLE": ["printable"],
|
||||
"ADDON": ["addon"],
|
||||
}
|
||||
|
||||
if task.status == "finished":
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+11
-6
@@ -679,6 +679,8 @@ def start_blenderkit_client():
|
||||
|
||||
def decide_client_binary_name() -> str:
|
||||
"""Decide the name of the BlenderKit-Client binary based on the current operating system and architecture.
|
||||
We unify the OS and CPU architecture naming to make it more accessible for general public.
|
||||
Darwin is renamed to MacOS. The CPU architecture is aligned to x86_64 or arm64.
|
||||
Possible return values:
|
||||
- blenderkit-client-windows-x86_64.exe
|
||||
- blenderkit-client-windows-arm64.exe
|
||||
@@ -687,14 +689,17 @@ def decide_client_binary_name() -> str:
|
||||
- blenderkit-client-macos-x86_64
|
||||
- blenderkit-client-macos-arm64
|
||||
"""
|
||||
os_name = platform.system()
|
||||
architecture = platform.machine()
|
||||
if os_name == "Darwin": # more user-friendly name for macOS
|
||||
os_name = platform.system().lower()
|
||||
if os_name == "darwin": # more user-friendly name for macOS
|
||||
os_name = "macos"
|
||||
if architecture == "AMD64": # fix for windows
|
||||
architecture = "x86_64"
|
||||
|
||||
if os_name == "Windows":
|
||||
architecture = platform.machine().lower()
|
||||
if architecture == "amd64": # align Windows convention
|
||||
architecture = "x86_64"
|
||||
elif architecture == "aarch64": # align Linux convention
|
||||
architecture = "arm64"
|
||||
|
||||
if os_name == "windows":
|
||||
return f"blenderkit-client-{os_name}-{architecture}.exe".lower()
|
||||
|
||||
return f"blenderkit-client-{os_name}-{architecture}".lower()
|
||||
|
||||
+13
-2
@@ -24,7 +24,18 @@ WHITE = (1, 1, 1, 0.9)
|
||||
TEXT = (0.9, 0.9, 0.9, 0.6)
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
|
||||
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
|
||||
PURPLE = (0.8, 0.4, 1.0, 1.0) # Full Plan purple
|
||||
GREEN_FREE = (0.4, 0.8, 0.4, 1.0) # Green for free addons
|
||||
"""Color for validator reports."""
|
||||
|
||||
GRAY = (0.7, 0.7, 0.7, 0.6)
|
||||
"""Default color for debug reports."""
|
||||
|
||||
# pure colors
|
||||
PURE_WHITE = (1, 1, 1, 1)
|
||||
PURE_BLACK = (0, 0, 0, 1)
|
||||
PURE_GREEN = (0, 1, 0, 1)
|
||||
PURE_RED = (1, 0, 0, 1)
|
||||
PURE_BLUE = (0, 0, 1, 1)
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ class BlenderKitDisclaimerOperator(BL_UI_OT_draw_operator):
|
||||
self.hover_bg_color = (0.127, 0.034, 1, 1.0)
|
||||
self.text_color = (0.9, 0.9, 0.9, 1)
|
||||
|
||||
print("@ BlenderKitDisclaimerOperator.__init__ message is: ", self.message)
|
||||
bk_logger.info("%s", self.message)
|
||||
|
||||
pix_size = get_text_size(
|
||||
font_id=1,
|
||||
text=self.message,
|
||||
|
||||
+765
-6
@@ -16,16 +16,22 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import addon_utils
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
from . import (
|
||||
append_link,
|
||||
client_lib,
|
||||
client_tasks,
|
||||
global_vars,
|
||||
paths,
|
||||
reports,
|
||||
resolutions,
|
||||
@@ -34,10 +40,10 @@ from . import (
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
import bpy
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
@@ -48,6 +54,232 @@ from bpy.props import (
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_blenderkit_repository():
|
||||
"""Find the BlenderKit extensions repository index.
|
||||
|
||||
Returns:
|
||||
int: Repository index if found, -1 otherwise
|
||||
"""
|
||||
enabled_repos = [
|
||||
repo for repo in bpy.context.preferences.extensions.repos if repo.enabled
|
||||
]
|
||||
for i, repo in enumerate(enabled_repos):
|
||||
if (
|
||||
repo.remote_url and global_vars.SERVER in repo.remote_url
|
||||
) or "blenderkit" in repo.name.lower():
|
||||
return repo, i
|
||||
return None, -1
|
||||
|
||||
|
||||
def get_addon_installation_status(asset_data):
|
||||
"""Get the installation and enablement status of an addon.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"installed": bool,
|
||||
"enabled": bool,
|
||||
"pkg_id": str,
|
||||
"cached_pkg": dict or None
|
||||
}
|
||||
"""
|
||||
|
||||
# Get the correct package ID
|
||||
extension_id = asset_data.get("dictParameters", {}).get("extensionId")
|
||||
if not extension_id:
|
||||
return {
|
||||
"installed": False,
|
||||
"enabled": False,
|
||||
"pkg_id": None,
|
||||
"cached_pkg": None,
|
||||
}
|
||||
|
||||
# Check if addon is installed and enabled using Blender's addon system
|
||||
|
||||
# Method 1: Check if it's in the enabled addons list
|
||||
# For new extension system, addons have format: bl_ext.repository_name.package_name
|
||||
enabled_addons = [addon.module for addon in bpy.context.preferences.addons]
|
||||
|
||||
# Check direct match first
|
||||
is_enabled = extension_id in enabled_addons
|
||||
|
||||
# If not found, check for extension format: bl_ext.www_blenderkit_com.package_name
|
||||
if not is_enabled:
|
||||
extension_module_name = f"bl_ext.www_blenderkit_com.{extension_id}"
|
||||
is_enabled = extension_module_name in enabled_addons
|
||||
bk_logger.info(
|
||||
f"Checking extension format: {extension_module_name} -> enabled: {is_enabled}"
|
||||
)
|
||||
|
||||
# Also try other possible repository name formats
|
||||
if not is_enabled:
|
||||
for addon_module in enabled_addons:
|
||||
if addon_module.endswith(
|
||||
f".{extension_id}"
|
||||
) and addon_module.startswith("bl_ext."):
|
||||
is_enabled = True
|
||||
bk_logger.info(
|
||||
f"Found enabled addon with extension format: {addon_module}"
|
||||
)
|
||||
break
|
||||
|
||||
# Method 2: Check if it's installed (may be disabled) using addon_utils
|
||||
is_installed = False
|
||||
try:
|
||||
for addon_module in addon_utils.modules():
|
||||
# Check direct match
|
||||
if addon_module.__name__ == extension_id:
|
||||
is_installed = True
|
||||
break
|
||||
# Check extension format match
|
||||
elif addon_module.__name__.endswith(
|
||||
f".{extension_id}"
|
||||
) and addon_module.__name__.startswith("bl_ext."):
|
||||
is_installed = True
|
||||
bk_logger.info(
|
||||
f"Found installed addon with extension format: {addon_module.__name__}"
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking addon_utils.modules(): {e}")
|
||||
|
||||
# If found through addon_utils, we know it's installed
|
||||
# But we need to double-check enabled status using the correct module name
|
||||
if is_installed and not is_enabled:
|
||||
# Try to find the correct module name format for this addon
|
||||
try:
|
||||
for addon_module in addon_utils.modules():
|
||||
if addon_module.__name__ == extension_id or (
|
||||
addon_module.__name__.endswith(f".{extension_id}")
|
||||
and addon_module.__name__.startswith("bl_ext.")
|
||||
):
|
||||
# Check if this specific module name is enabled
|
||||
is_enabled = addon_module.__name__ in enabled_addons
|
||||
if is_enabled:
|
||||
bk_logger.info(f"Found enabled addon: {addon_module.__name__}")
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error double-checking enabled status: {e}")
|
||||
|
||||
# Method 3: If not found through traditional addon system, check extensions system
|
||||
if not is_installed:
|
||||
try:
|
||||
override_extension_draw.ensure_repo_cache()
|
||||
bk_ext_cache = bpy.context.window_manager.get(
|
||||
"blenderkit_extensions_repo_cache", {}
|
||||
)
|
||||
|
||||
for cache_key, pkg_data in bk_ext_cache.items():
|
||||
if isinstance(pkg_data, dict) and pkg_data.get("id") == extension_id:
|
||||
# Check if it's actually installed in the extension system
|
||||
is_installed = pkg_data.get("installed", False)
|
||||
# For extensions, enabled status might be in the cache
|
||||
if is_installed and not is_enabled:
|
||||
is_enabled = pkg_data.get("enabled", False)
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking extension cache: {e}")
|
||||
|
||||
# Method 4: Check through Blender's extension repositories directly
|
||||
if not is_installed:
|
||||
try:
|
||||
|
||||
# Look for BlenderKit repository and check its packages
|
||||
for repo in bpy.context.preferences.extensions.repos:
|
||||
if not repo.enabled:
|
||||
continue
|
||||
if not (
|
||||
(repo.remote_url and global_vars.SERVER in repo.remote_url)
|
||||
or "blenderkit" in repo.name.lower()
|
||||
):
|
||||
continue
|
||||
|
||||
# This is a BlenderKit repository, try to find our package
|
||||
# Note: The actual package checking would require deeper access to the repository data
|
||||
# For now, we'll rely on the previous methods
|
||||
break
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error checking extension repositories: {e}")
|
||||
|
||||
# Debug: Show some enabled addons for reference
|
||||
blenderkit_addons = [
|
||||
addon
|
||||
for addon in enabled_addons
|
||||
if "blenderkit" in addon.lower() or addon.endswith(extension_id)
|
||||
]
|
||||
if blenderkit_addons:
|
||||
bk_logger.info(f"Found BlenderKit-related enabled addons: {blenderkit_addons}")
|
||||
|
||||
bk_logger.info(
|
||||
f"Addon status check for '{extension_id}': installed={is_installed}, enabled={is_enabled}"
|
||||
)
|
||||
|
||||
return {
|
||||
"installed": is_installed,
|
||||
"enabled": is_enabled,
|
||||
"pkg_id": extension_id,
|
||||
"cached_pkg": None, # Not using cached_pkg anymore
|
||||
}
|
||||
|
||||
|
||||
def install_addon_from_local_file(asset_data, file_path, enable_on_install=True):
|
||||
"""Install an addon from a local zip file using Blender's extensions API.
|
||||
|
||||
Args:
|
||||
asset_data: Asset metadata dictionary
|
||||
file_path: Path to the downloaded zip file
|
||||
enable_on_install: If True, enable the addon after installation (default: True)
|
||||
"""
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
|
||||
if bpy.app.version < (4, 2, 0):
|
||||
error_msg = f"Addon installation requires Blender 4.2 or newer. Current version: {'.'.join(map(str, bpy.app.version[:2]))}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"Addon file not found: {file_path}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
bk_logger.info(f"Installing addon '{addon_name}' from local file: {file_path}")
|
||||
|
||||
status = get_addon_installation_status(asset_data)
|
||||
if status["installed"]:
|
||||
reports.add_report(f"Addon '{addon_name}' is already installed", type="INFO")
|
||||
return
|
||||
|
||||
# Find the BlenderKit repository to install the addon to
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
error_msg = "BlenderKit repository not found. Please ensure the BlenderKit extensions repository is enabled in preferences."
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# Install from file to the BlenderKit repository
|
||||
result = bpy.ops.extensions.package_install_files(
|
||||
repo=repo.module,
|
||||
filepath=file_path,
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Installation failed - operation returned: {result}")
|
||||
|
||||
post_install_status = get_addon_installation_status(asset_data)
|
||||
if not post_install_status["installed"]:
|
||||
raise Exception(
|
||||
f"Installation verification failed: '{addon_name}' was not installed. "
|
||||
f"This may be due to version compatibility issues or other requirements not being met."
|
||||
)
|
||||
|
||||
status_text = "enabled" if enable_on_install else "disabled"
|
||||
reports.add_report(
|
||||
f"Successfully installed addon '{addon_name}' ({status_text})", type="INFO"
|
||||
)
|
||||
|
||||
|
||||
download_tasks = {}
|
||||
|
||||
@@ -117,6 +349,68 @@ def check_unused():
|
||||
l.user_clear()
|
||||
|
||||
|
||||
def get_temp_enabled_addons():
|
||||
"""Get list of temporarily enabled addons from preferences."""
|
||||
|
||||
try:
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
temp_addons_json = prefs.temp_enabled_addons
|
||||
return json.loads(temp_addons_json)
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error reading temporary addons from preferences: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def set_temp_enabled_addons(addon_list):
|
||||
"""Save list of temporarily enabled addons to preferences."""
|
||||
|
||||
try:
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
prefs.temp_enabled_addons = json.dumps(addon_list)
|
||||
bk_logger.info(f"Saved {len(addon_list)} temporary addons to preferences")
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Error saving temporary addons to preferences: {e}")
|
||||
|
||||
|
||||
def add_temp_enabled_addon(pkg_id):
|
||||
"""Add an addon to the temporary enabled list."""
|
||||
temp_enabled = get_temp_enabled_addons()
|
||||
if pkg_id not in temp_enabled:
|
||||
temp_enabled.append(pkg_id)
|
||||
set_temp_enabled_addons(temp_enabled)
|
||||
bk_logger.info(f"Added {pkg_id} to temporary addons list")
|
||||
|
||||
|
||||
def cleanup_temp_enabled_addons():
|
||||
"""Disable temporarily enabled addons."""
|
||||
|
||||
try:
|
||||
temp_enabled = get_temp_enabled_addons()
|
||||
|
||||
if not temp_enabled:
|
||||
bk_logger.info("No temporarily enabled addons to clean up")
|
||||
return
|
||||
|
||||
bk_logger.info(f"Cleaning up {len(temp_enabled)} temporarily enabled addons")
|
||||
|
||||
# Disable all temporarily enabled addons using preferences API
|
||||
for pkg_id in temp_enabled:
|
||||
try:
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
bpy.ops.preferences.addon_disable(module=full_module_name)
|
||||
bk_logger.info(f"Disabled temporarily enabled addon: {pkg_id}")
|
||||
except Exception as e:
|
||||
bk_logger.warning(
|
||||
f"Failed to disable temporarily enabled addon {pkg_id}: {e}"
|
||||
)
|
||||
|
||||
# Clear the list in preferences
|
||||
set_temp_enabled_addons([])
|
||||
bk_logger.info("Temporary addon cleanup completed")
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Error during temporary addon cleanup: {e}")
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_save(context):
|
||||
"""Do cleanup of blenderkit props and send a message to the server about assets used."""
|
||||
@@ -131,6 +425,49 @@ def scene_save(context):
|
||||
client_lib.report_usages(report_data)
|
||||
|
||||
|
||||
def refresh_addon_search_results_status():
|
||||
"""Refresh installation status in addon search results after installation operations."""
|
||||
|
||||
try:
|
||||
# Get current search results
|
||||
sr = search.get_search_results()
|
||||
if not sr:
|
||||
return
|
||||
|
||||
# Check if we're currently viewing addons
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != "ADDON":
|
||||
return
|
||||
|
||||
# Update installation status for all addon search results
|
||||
for asset_data in sr:
|
||||
if asset_data.get("assetType") == "addon":
|
||||
try:
|
||||
status = get_addon_installation_status(asset_data)
|
||||
is_installed = status.get("installed", False)
|
||||
is_enabled = status.get("enabled", False)
|
||||
|
||||
# Update the status in search results
|
||||
asset_data["downloaded"] = 100 if is_installed else 0
|
||||
asset_data["enabled"] = is_enabled
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.warning(
|
||||
f"Could not refresh status for addon {asset_data.get('name', 'Unknown')}: {e}"
|
||||
)
|
||||
asset_data["downloaded"] = 0
|
||||
asset_data["enabled"] = False
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Error refreshing addon search results status: {e}")
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_load_pre(context):
|
||||
"""Clean up temporarily enabled addons before loading new file."""
|
||||
cleanup_temp_enabled_addons()
|
||||
|
||||
|
||||
@persistent
|
||||
def scene_load(context):
|
||||
"""Restart broken downloads on scene load."""
|
||||
@@ -636,7 +973,8 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
|
||||
asset_data["resolution"] = kwargs["resolution"]
|
||||
udpate_asset_data_in_dicts(asset_data)
|
||||
update_asset_metadata(asset_main, asset_data)
|
||||
if asset_main is not None:
|
||||
update_asset_metadata(asset_main, asset_data)
|
||||
|
||||
bpy.ops.ed.undo_push(
|
||||
"INVOKE_REGION_WIN", message="add %s to scene" % asset_data["name"]
|
||||
@@ -949,6 +1287,25 @@ def download_post(task: client_tasks.Task) -> None:
|
||||
return
|
||||
|
||||
orig_task.update(task.data)
|
||||
|
||||
# For addons, install from the downloaded file instead of appending
|
||||
if at == "addon":
|
||||
if file_paths:
|
||||
# Check if addon should be enabled after installation (default: True)
|
||||
enable_on_install = task.data.get("enable_on_install", True)
|
||||
install_addon_from_local_file(
|
||||
task.data["asset_data"],
|
||||
file_paths[-1],
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
|
||||
else:
|
||||
bk_logger.error("No file paths available for addon installation")
|
||||
reports.add_report(
|
||||
"Addon download completed but no file found", type="ERROR"
|
||||
)
|
||||
return
|
||||
|
||||
try_finished_append(
|
||||
file_paths=file_paths, **task.data
|
||||
) # exception is handled in calling function
|
||||
@@ -966,7 +1323,6 @@ def download(asset_data, **kwargs):
|
||||
report = f"Maximum retries exceeded for {asset_data['name']}"
|
||||
sprops.report = report
|
||||
reports.add_report(report, type="ERROR")
|
||||
|
||||
bk_logger.debug(sprops.report)
|
||||
return
|
||||
|
||||
@@ -980,6 +1336,8 @@ def download(asset_data, **kwargs):
|
||||
# inject resolution into prefs.
|
||||
prefs = utils.get_preferences_as_dict()
|
||||
prefs["resolution"] = kwargs.get("resolution", "original")
|
||||
if "unpack_files" in kwargs: # for add-on download
|
||||
prefs["unpack_files"] = kwargs["unpack_files"]
|
||||
|
||||
data = {
|
||||
"asset_data": asset_data,
|
||||
@@ -1271,10 +1629,391 @@ asset_types = (
|
||||
("MATERIAL", "Material", "any .blend Material"),
|
||||
("TEXTURE", "Texture", "a texture, or texture set"),
|
||||
("BRUSH", "Brush", "brush, can be any type of blender brush"),
|
||||
("ADDON", "Addon", "addnon"),
|
||||
("ADDON", "Addon", "addon"),
|
||||
)
|
||||
|
||||
|
||||
class BlenderkitAddonManagerOperator(bpy.types.Operator):
|
||||
"""Manage BlenderKit addon installation, enabling, and disabling"""
|
||||
|
||||
bl_idname = "scene.blenderkit_addon_manager"
|
||||
bl_label = "Addon Manager"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
asset_data: bpy.props.StringProperty() # JSON encoded asset data
|
||||
action: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("INSTALL", "Install", "Install the addon"),
|
||||
("UNINSTALL", "Uninstall", "Uninstall the addon"),
|
||||
("ENABLE", "Enable", "Enable the addon"),
|
||||
("DISABLE", "Disable", "Disable the addon"),
|
||||
("TEMP_ENABLE", "Enable Temporarily", "Enable until end of session"),
|
||||
]
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
reports.add_report("Invalid asset data", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
pkg_id = status["pkg_id"]
|
||||
|
||||
# For non-install actions, we need the repository and pkg_id
|
||||
repo_index = -1
|
||||
if self.action != "INSTALL":
|
||||
if not pkg_id:
|
||||
reports.add_report("No extension ID found for this addon", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Find the BlenderKit repository
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
reports.add_report("BlenderKit repository not found", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
if self.action == "INSTALL":
|
||||
# Trigger download which will automatically install after completion
|
||||
reports.add_report(f"Downloading addon '{addon_name}'...", type="INFO")
|
||||
|
||||
# Check if addon is already downloading
|
||||
if check_downloading(asset_data):
|
||||
reports.add_report(
|
||||
f"Addon '{addon_name}' is already being downloaded", type="INFO"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
# Start the download
|
||||
download(asset_data, resolution="blend")
|
||||
return {"FINISHED"}
|
||||
|
||||
elif self.action == "UNINSTALL":
|
||||
result = bpy.ops.extensions.package_uninstall(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Uninstallation failed - operation returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully uninstalled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully uninstalled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "ENABLE":
|
||||
result = bpy.ops.extensions.package_enable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Enable failed - operation returned: {result}")
|
||||
reports.add_report(f"Successfully enabled '{addon_name}'", type="INFO")
|
||||
self.report({"INFO"}, f"Successfully enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "DISABLE":
|
||||
result = bpy.ops.extensions.package_disable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Disable failed - operation returned: {result}")
|
||||
reports.add_report(f"Successfully disabled '{addon_name}'", type="INFO")
|
||||
self.report({"INFO"}, f"Successfully disabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif self.action == "TEMP_ENABLE":
|
||||
result = bpy.ops.extensions.package_enable(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Temporary enable failed - operation returned: {result}"
|
||||
)
|
||||
# Store the package for later disabling
|
||||
wm = context.window_manager
|
||||
temp_enabled = wm.get("blenderkit_temp_enabled_addons", [])
|
||||
if pkg_id not in temp_enabled:
|
||||
temp_enabled.append(pkg_id)
|
||||
wm["blenderkit_temp_enabled_addons"] = temp_enabled
|
||||
reports.add_report(
|
||||
f"Temporarily enabled '{addon_name}' (will disable on session end)",
|
||||
type="INFO",
|
||||
)
|
||||
self.report({"INFO"}, f"Temporarily enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to {self.action.lower()} '{addon_name}': {e}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderkitAddonChoiceOperator(bpy.types.Operator):
|
||||
"""Show addon management options popup"""
|
||||
|
||||
bl_idname = "scene.blenderkit_addon_choice"
|
||||
bl_label = "Addon Options"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
asset_data: bpy.props.StringProperty() # JSON encoded asset data
|
||||
|
||||
# Actions for not installed addons
|
||||
action_not_installed: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
(
|
||||
"INSTALL_AND_ENABLE",
|
||||
"Install and Enable",
|
||||
"Install the addon and enable it immediately",
|
||||
"CHECKBOX_HLT",
|
||||
0,
|
||||
),
|
||||
(
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"Install and Enable Temporarily",
|
||||
"Install and enable until end of session",
|
||||
"TIME",
|
||||
1,
|
||||
),
|
||||
(
|
||||
"INSTALL_ONLY",
|
||||
"Install Only",
|
||||
"Install the addon but keep it disabled",
|
||||
"IMPORT",
|
||||
2,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Actions for installed and enabled addons
|
||||
action_installed_enabled: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
("DISABLE", "Disable", "Disable the addon", "CHECKBOX_DEHLT", 0),
|
||||
("UNINSTALL", "Uninstall", "Completely remove the addon", "CANCEL", 1),
|
||||
],
|
||||
)
|
||||
|
||||
# Actions for installed but disabled addons
|
||||
action_installed_disabled: bpy.props.EnumProperty(
|
||||
name="Action",
|
||||
description="Choose what to do with this addon",
|
||||
items=[
|
||||
("ENABLE", "Enable", "Enable the addon permanently", "CHECKBOX_HLT", 0),
|
||||
(
|
||||
"TEMP_ENABLE",
|
||||
"Enable Temporarily",
|
||||
"Enable until end of session",
|
||||
"TIME",
|
||||
1,
|
||||
),
|
||||
("UNINSTALL", "Uninstall", "Completely remove the addon", "CANCEL", 2),
|
||||
],
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
layout = self.layout
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
layout.label(text="Invalid asset data")
|
||||
return
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
|
||||
layout.label(text=f"Addon: {addon_name}")
|
||||
layout.separator()
|
||||
|
||||
layout = layout.column()
|
||||
# Show current status and appropriate action enum
|
||||
if not status["installed"]:
|
||||
layout.label(text="Status: Not Installed", icon="QUESTION")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_not_installed", expand=True)
|
||||
elif status["enabled"]:
|
||||
layout.label(text="Status: Installed and Enabled", icon="CHECKMARK")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_installed_enabled", expand=True)
|
||||
else:
|
||||
layout.label(text="Status: Installed but Disabled", icon="X")
|
||||
layout.separator()
|
||||
layout.prop(self, "action_installed_disabled", expand=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Set default values for each enum
|
||||
self.action_not_installed = "INSTALL_AND_ENABLE"
|
||||
self.action_installed_enabled = "DISABLE"
|
||||
self.action_installed_disabled = "ENABLE"
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=350)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
try:
|
||||
asset_data = json.loads(self.asset_data)
|
||||
except:
|
||||
reports.add_report("Invalid asset data", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
addon_name = asset_data.get("name", "Unknown Addon")
|
||||
status = get_addon_installation_status(asset_data)
|
||||
pkg_id = status["pkg_id"]
|
||||
|
||||
# Get the selected action based on addon status
|
||||
if not status["installed"]:
|
||||
selected_action = self.action_not_installed
|
||||
elif status["enabled"]:
|
||||
selected_action = self.action_installed_enabled
|
||||
else:
|
||||
selected_action = self.action_installed_disabled
|
||||
|
||||
# For non-install actions, we need the repository and pkg_id
|
||||
repo_index = -1
|
||||
if selected_action not in (
|
||||
"INSTALL_AND_ENABLE",
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"INSTALL_ONLY",
|
||||
):
|
||||
if not pkg_id:
|
||||
reports.add_report("No extension ID found for this addon", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Find the BlenderKit repository
|
||||
repo, repo_index = get_blenderkit_repository()
|
||||
if repo is None:
|
||||
reports.add_report("BlenderKit repository not found", type="ERROR")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
if selected_action in (
|
||||
"INSTALL_AND_ENABLE",
|
||||
"INSTALL_AND_TEMP_ENABLE",
|
||||
"INSTALL_ONLY",
|
||||
):
|
||||
# Trigger download which will automatically install and enable after completion
|
||||
reports.add_report(f"Downloading addon '{addon_name}'...", type="INFO")
|
||||
|
||||
# Check if addon is already downloading
|
||||
if check_downloading(asset_data):
|
||||
reports.add_report(
|
||||
f"Addon '{addon_name}' is already being downloaded", type="INFO"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
if selected_action == "INSTALL_AND_TEMP_ENABLE":
|
||||
add_temp_enabled_addon(pkg_id)
|
||||
|
||||
# Enable on install for both INSTALL_AND_ENABLE and INSTALL_AND_TEMP_ENABLE
|
||||
enable_on_install = selected_action != "INSTALL_ONLY"
|
||||
# Start the download, disable unpacking
|
||||
download(
|
||||
asset_data,
|
||||
resolution="blend",
|
||||
unpack_files=False,
|
||||
enable_on_install=enable_on_install,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
elif selected_action == "UNINSTALL":
|
||||
result = bpy.ops.extensions.package_uninstall(
|
||||
repo_index=repo_index, pkg_id=pkg_id
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Uninstallation failed - operation returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully uninstalled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully uninstalled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
elif selected_action == "ENABLE":
|
||||
# Enable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_enable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(f"Enable operation failed - returned: {result}")
|
||||
reports.add_report(
|
||||
f"Successfully enabled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to enable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to enable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
|
||||
elif selected_action == "DISABLE":
|
||||
# Disable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_disable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Disable operation failed - returned: {result}"
|
||||
)
|
||||
reports.add_report(
|
||||
f"Successfully disabled '{addon_name}'", type="INFO"
|
||||
)
|
||||
self.report({"INFO"}, f"Successfully disabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to disable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to disable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
|
||||
elif selected_action == "TEMP_ENABLE":
|
||||
# Temporarily enable using preferences API
|
||||
full_module_name = f"bl_ext.www_blenderkit_com.{pkg_id}"
|
||||
try:
|
||||
result = bpy.ops.preferences.addon_enable(module=full_module_name)
|
||||
if "FINISHED" not in result:
|
||||
raise Exception(
|
||||
f"Temporary enable operation failed - returned: {result}"
|
||||
)
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Failed to temp enable addon: {e}")
|
||||
reports.add_report(
|
||||
f"Failed to enable '{addon_name}': {e}", type="ERROR"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Store the package for later disabling
|
||||
add_temp_enabled_addon(pkg_id)
|
||||
reports.add_report(
|
||||
f"Temporarily enabled '{addon_name}' (will disable on session end)",
|
||||
type="INFO",
|
||||
)
|
||||
self.report({"INFO"}, f"Temporarily enabled '{addon_name}'")
|
||||
refresh_addon_search_results_status()
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Addon operation failed for '{addon_name}': {e}")
|
||||
error_msg = f"Failed to {selected_action.lower().replace('_', ' ')} '{addon_name}': {e}"
|
||||
reports.add_report(error_msg, type="ERROR")
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderkitKillDownloadOperator(bpy.types.Operator):
|
||||
"""Kill a download"""
|
||||
|
||||
@@ -1315,6 +2054,10 @@ def available_resolutions_callback(self, context):
|
||||
|
||||
def has_asset_files(asset_data):
|
||||
"""Check if asset has files."""
|
||||
# Addons are handled separately by the extension system
|
||||
if asset_data["assetType"] == "addon":
|
||||
return True
|
||||
|
||||
for f in asset_data["files"]:
|
||||
if f["fileType"] in ("blend", "zip_file"):
|
||||
return True
|
||||
@@ -1491,6 +2234,14 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
asset_type = self.asset_data["assetType"]
|
||||
|
||||
# Handle addon assets with popup
|
||||
if asset_type == "addon":
|
||||
|
||||
bpy.ops.scene.blenderkit_addon_choice(
|
||||
"INVOKE_DEFAULT", asset_data=json.dumps(self.asset_data)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
if (
|
||||
(asset_type == "model" or asset_type == "material")
|
||||
and (bpy.context.mode != "OBJECT")
|
||||
@@ -1636,12 +2387,20 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
||||
def register_download():
|
||||
bpy.utils.register_class(BlenderkitDownloadOperator)
|
||||
bpy.utils.register_class(BlenderkitKillDownloadOperator)
|
||||
# bpy.utils.register_class(BlenderkitAddonManagerOperator) # Replaced by BlenderkitAddonChoiceOperator
|
||||
bpy.utils.register_class(BlenderkitAddonChoiceOperator)
|
||||
bpy.app.handlers.load_post.append(scene_load)
|
||||
bpy.app.handlers.save_pre.append(scene_save)
|
||||
bpy.app.handlers.load_post.append(scene_load_pre)
|
||||
|
||||
|
||||
def unregister_download():
|
||||
bpy.utils.unregister_class(BlenderkitDownloadOperator)
|
||||
bpy.utils.unregister_class(BlenderkitKillDownloadOperator)
|
||||
# bpy.utils.unregister_class(BlenderkitAddonManagerOperator) # Replaced by BlenderkitAddonChoiceOperator
|
||||
bpy.utils.unregister_class(BlenderkitAddonChoiceOperator)
|
||||
bpy.app.handlers.load_post.remove(scene_load)
|
||||
bpy.app.handlers.save_pre.remove(scene_save)
|
||||
bpy.app.handlers.load_post.remove(scene_load_pre)
|
||||
# Clean up any remaining temporarily enabled addons
|
||||
cleanup_temp_enabled_addons()
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ from typing import Any, Optional
|
||||
from . import datas
|
||||
|
||||
|
||||
CLIENT_VERSION = "v1.6.0"
|
||||
CLIENT_VERSION = "v1.7.0"
|
||||
CLIENT_ACCESSIBLE = False
|
||||
"""Is Client accessible? Can add-on access it and call stuff which uses it?"""
|
||||
CLIENT_RUNNING = False
|
||||
|
||||
+70
-7
@@ -18,10 +18,26 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IMG:
|
||||
name: str
|
||||
filepath: str
|
||||
|
||||
def gl_load(self):
|
||||
"""Imitates bpy.types.Image.gl_load() behavior."""
|
||||
return None
|
||||
|
||||
|
||||
def get_orig_render_settings():
|
||||
rs = bpy.context.scene.render
|
||||
ims = rs.image_settings
|
||||
@@ -91,21 +107,68 @@ def set_colorspace(img, colorspace: str = ""):
|
||||
if colorspace == "":
|
||||
colorspace = guess_colorspace()
|
||||
|
||||
if colorspace == "Non-Color":
|
||||
img.colorspace_settings.is_data = True
|
||||
else:
|
||||
img.colorspace_settings.name = colorspace
|
||||
except Exception as e:
|
||||
print(f"Colorspace {colorspace} not found: {e}")
|
||||
if hasattr(img, "colorspace_settings") and colorspace:
|
||||
if colorspace == "Non-Color":
|
||||
img.colorspace_settings.is_data = True
|
||||
else:
|
||||
img.colorspace_settings.name = colorspace
|
||||
|
||||
except Exception:
|
||||
bk_logger.exception("Colorspace '%s' not found: ", colorspace)
|
||||
|
||||
|
||||
def guess_colorspace():
|
||||
@lru_cache(maxsize=1)
|
||||
def list_available_image_colorspaces():
|
||||
"""Lists available color spaces in blender by creating a temporary image if needed.
|
||||
|
||||
Returns:
|
||||
List of color space names.
|
||||
"""
|
||||
# Check if there are existing images
|
||||
temp_image = None
|
||||
if bpy.data.images:
|
||||
img = bpy.data.images[0]
|
||||
else:
|
||||
# Create temporary image
|
||||
temp_image = bpy.data.images.new(
|
||||
"TempImage_ForColorSpaceList", width=1, height=1
|
||||
)
|
||||
img = temp_image
|
||||
|
||||
# Get available color spaces
|
||||
color_spaces = [
|
||||
cs.identifier
|
||||
for cs in img.colorspace_settings.bl_rna.properties["name"].enum_items
|
||||
]
|
||||
|
||||
# Clean up temporary image if created
|
||||
if temp_image:
|
||||
bpy.data.images.remove(temp_image)
|
||||
|
||||
return color_spaces
|
||||
|
||||
|
||||
def guess_colorspace() -> str:
|
||||
"""Tries to guess the colorspace from the current display device and available color spaces."""
|
||||
display_device = bpy.context.scene.display_settings.display_device
|
||||
if display_device == "sRGB":
|
||||
return "sRGB"
|
||||
if display_device == "ACES":
|
||||
return "aces"
|
||||
|
||||
# detect available color spaces on image data
|
||||
all_clr_spaces = list_available_image_colorspaces()
|
||||
|
||||
# try to match display device with color space
|
||||
for cs in all_clr_spaces:
|
||||
if display_device.lower() in cs.lower():
|
||||
return cs
|
||||
|
||||
# fallback
|
||||
if "sRGB" in all_clr_spaces:
|
||||
return "sRGB"
|
||||
return ""
|
||||
|
||||
|
||||
def analyze_image_is_true_hdr(image):
|
||||
import numpy
|
||||
|
||||
+60
-35
@@ -9,6 +9,7 @@ The original method is then called from the new method, with the same arguments,
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
from . import icons
|
||||
|
||||
@@ -20,6 +21,8 @@ from bpy.types import Operator
|
||||
|
||||
EXTENSIONS_API_URL = "https://www.blenderkit.com/api/v1/extensions/"
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- New Modal Operator ---
|
||||
class BK_OT_buy_extension_and_watch(Operator):
|
||||
@@ -56,7 +59,7 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
# Open the URL
|
||||
try:
|
||||
bpy.ops.wm.url_open(url=self.url)
|
||||
print(f"BlenderKit: Opening buy 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?
|
||||
@@ -72,10 +75,11 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
self._last_refresh_time = (
|
||||
self._start_time
|
||||
) # Initialize to avoid immediate refresh
|
||||
print(
|
||||
f"BlenderKit: Started watching repository index {self.repo_index} for updates."
|
||||
bk_logger.info(
|
||||
"Started watching repository index %s for updates.", self.repo_index
|
||||
)
|
||||
context.area.tag_redraw() # Update UI to show operator is running if needed
|
||||
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):
|
||||
@@ -84,19 +88,19 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
# --- Exit Conditions ---
|
||||
# 1. User closed Preferences or changed area
|
||||
if context.area is None or context.area.type != "PREFERENCES":
|
||||
print("BlenderKit: Preferences window closed or changed, stopping watcher.")
|
||||
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:
|
||||
print("BlenderKit: Watcher timed out, stopping.")
|
||||
bk_logger.info("Watcher timed out, stopping.")
|
||||
self.cancel(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 3. User cancellation
|
||||
if event.type in {"RIGHTMOUSE", "ESC"}:
|
||||
print("BlenderKit: Watcher cancelled by user.")
|
||||
bk_logger.info("Watcher cancelled by user.")
|
||||
self.cancel(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -104,24 +108,25 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
if event.type == "TIMER":
|
||||
# Check if refresh interval has passed
|
||||
if current_time - self._last_refresh_time >= self._refresh_interval:
|
||||
print(
|
||||
f"BlenderKit: Refresh interval reached, attempting sync for repo index {self.repo_index}..."
|
||||
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)
|
||||
print(
|
||||
f"BlenderKit: repo_sync called for index {self.repo_index}."
|
||||
bk_logger.info(
|
||||
"repo_sync called for index %s.", self.repo_index
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"BlenderKit: Repository index {self.repo_index} no longer valid."
|
||||
bk_logger.info(
|
||||
"Repository index %s no longer valid.", self.repo_index
|
||||
)
|
||||
# Optionally cancel here if repo is gone
|
||||
except Exception as e:
|
||||
except:
|
||||
# This might fail if another operation is in progress
|
||||
print(f"BlenderKit: extensions.repo_sync failed: {e}")
|
||||
bk_logger.exception("extensions.repo_sync failed.")
|
||||
finally:
|
||||
self._last_refresh_time = (
|
||||
current_time # Reset timer regardless of success
|
||||
@@ -134,13 +139,32 @@ class BK_OT_buy_extension_and_watch(Operator):
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
print("BlenderKit: Watcher timer removed.")
|
||||
context.area.tag_redraw() # Update UI
|
||||
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,
|
||||
*,
|
||||
@@ -163,14 +187,17 @@ def extension_draw_item_blenderkit(
|
||||
cache_reloaded = ensure_repo_cache()
|
||||
if cache_reloaded:
|
||||
# If cache was just reloaded, tag UI for redraw
|
||||
layout.tag_redraw()
|
||||
print("BlenderKit: Cache reloaded, tagging layout 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
|
||||
print(
|
||||
"BlenderKit: Extension cache not available in window_manager after ensure_repo_cache call."
|
||||
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
|
||||
@@ -510,7 +537,7 @@ def clear_repo_cache():
|
||||
|
||||
|
||||
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.
|
||||
@@ -525,10 +552,10 @@ def ensure_repo_cache():
|
||||
# If repo doesn't exist, clear cache if it exists in window manager
|
||||
if cache_key in wm:
|
||||
del wm[cache_key]
|
||||
print(f"BlenderKit: Cleared stale extension cache for missing repository.")
|
||||
bk_logger.info("Cleared stale extension cache for missing repository.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
print(f"BlenderKit Debug: Repository not found, exiting check.")
|
||||
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
|
||||
@@ -541,13 +568,11 @@ def ensure_repo_cache():
|
||||
if os.path.exists(cache_file):
|
||||
current_mtime = os.path.getmtime(cache_file)
|
||||
except OSError as e: # Handle potential race condition or permission issue
|
||||
print(
|
||||
f"BlenderKit: Warning - Could not get modification time for {cache_file}: {e}"
|
||||
)
|
||||
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]
|
||||
print(f"BlenderKit: Cleared extension cache due to mtime access error.")
|
||||
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
|
||||
@@ -601,7 +626,7 @@ def ensure_repo_cache():
|
||||
): # Ensure pkg is a dict and 'id' key exists
|
||||
new_cache[pkg["id"][:32]] = pkg
|
||||
else:
|
||||
print(f"BlenderKit: Skipping invalid package entry in cache: {pkg}")
|
||||
bk_logger.info("Skipping invalid package entry in cache: %s.", pkg)
|
||||
|
||||
wm[cache_key] = new_cache
|
||||
wm[mtime_key] = current_mtime # Update mtime only on successful load
|
||||
@@ -609,21 +634,21 @@ def ensure_repo_cache():
|
||||
reloaded_flag = True # Mark that we reloaded successfully
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(
|
||||
f"BlenderKit: Error decoding JSON from {cache_file}. Cache not loaded/updated."
|
||||
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]
|
||||
print("BlenderKit: Cleared cache due to JSON error.")
|
||||
bk_logger.info("Cleared cache due to JSON error.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
except Exception as e:
|
||||
print(f"BlenderKit: Error reading or processing cache file {cache_file}: {e}")
|
||||
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]
|
||||
print("BlenderKit: Cleared cache due to file processing error.")
|
||||
bk_logger.info("Cleared cache due to file processing error.")
|
||||
if mtime_key in wm:
|
||||
del wm[mtime_key]
|
||||
|
||||
|
||||
+2
@@ -45,6 +45,7 @@ BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = (
|
||||
BLENDERKIT_BRUSH_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-brush/"
|
||||
BLENDERKIT_HDR_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-hdr/"
|
||||
BLENDERKIT_SCENE_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/uploading-scene/"
|
||||
BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/add-ons-upload-beta/"
|
||||
BLENDERKIT_LOGIN_URL = f"{global_vars.SERVER}/accounts/login"
|
||||
BLENDERKIT_SIGNUP_URL = f"{global_vars.SERVER}/accounts/register"
|
||||
|
||||
@@ -152,6 +153,7 @@ def get_download_dirs(asset_type):
|
||||
"hdr": "hdrs",
|
||||
"nodegroup": "nodegroups",
|
||||
"printable": "printables",
|
||||
"addon": "addons",
|
||||
}
|
||||
|
||||
dirs = []
|
||||
|
||||
+281
-58
@@ -2,44 +2,62 @@
|
||||
# It is not intended for manual editing.
|
||||
|
||||
[metadata]
|
||||
groups = ["default"]
|
||||
groups = ["default", "dev"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:c76a62dd151343ab8bade20c52ba9ab72a8c97afba4d30e648c98eb624af333e"
|
||||
content_hash = "sha256:e66407ebe96aea59816d07e7d141f2564da883f1073e87bb85c544e60644b85a"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = ">=3.10"
|
||||
requires_python = ">=3.10,<3.13"
|
||||
|
||||
[[package]]
|
||||
name = "bandit"
|
||||
version = "1.8.6"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Security oriented static analyser for python code."
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"PyYAML>=5.3.1",
|
||||
"colorama>=0.3.9; platform_system == \"Windows\"",
|
||||
"rich",
|
||||
"stevedore>=1.20.0",
|
||||
]
|
||||
files = [
|
||||
{file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"},
|
||||
{file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.1.0"
|
||||
requires_python = ">=3.8"
|
||||
version = "25.9.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "The uncompromising code formatter."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"click>=8.0.0",
|
||||
"mypy-extensions>=0.4.3",
|
||||
"packaging>=22.0",
|
||||
"pathspec>=0.9.0",
|
||||
"platformdirs>=2",
|
||||
"pytokens>=0.1.10",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
"typing-extensions>=4.0.1; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "black-24.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94d5280d020dadfafc75d7cae899609ed38653d3f5e82e7ce58f75e76387ed3d"},
|
||||
{file = "black-24.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aaf9aa85aaaa466bf969e7dd259547f4481b712fe7ee14befeecc152c403ee05"},
|
||||
{file = "black-24.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec489cae76eac3f7573629955573c3a0e913641cafb9e3bfc87d8ce155ebdb29"},
|
||||
{file = "black-24.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5a0100b4bdb3744dd68412c3789f472d822dc058bb3857743342f8d7f93a5a7"},
|
||||
{file = "black-24.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6cc5a6ba3e671cfea95a40030b16a98ee7dc2e22b6427a6f3389567ecf1b5262"},
|
||||
{file = "black-24.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0e367759062dcabcd9a426d12450c6d61faf1704a352a49055a04c9f9ce8f5a"},
|
||||
{file = "black-24.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be305563ff4a2dea813f699daaffac60b977935f3264f66922b1936a5e492ee4"},
|
||||
{file = "black-24.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a8977774929b5db90442729f131221e58cc5d8208023c6af9110f26f75b6b20"},
|
||||
{file = "black-24.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d74d4d0da276fbe3b95aa1f404182562c28a04402e4ece60cf373d0b902f33a0"},
|
||||
{file = "black-24.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39addf23f7070dbc0b5518cdb2018468ac249d7412a669b50ccca18427dba1f3"},
|
||||
{file = "black-24.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827a7c0da520dd2f8e6d7d3595f4591aa62ccccce95b16c0e94bb4066374c4c2"},
|
||||
{file = "black-24.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0cd59d01bf3306ff7e3076dd7f4435fcd2fafe5506a6111cae1138fc7de52382"},
|
||||
{file = "black-24.1.0-py3-none-any.whl", hash = "sha256:5134a6f6b683aa0a5592e3fd61dd3519d8acd953d93e2b8b76f9981245b65594"},
|
||||
{file = "black-24.1.0.tar.gz", hash = "sha256:30fbf768cd4f4576598b1db0202413fafea9a227ef808d1a12230c643cefe9fc"},
|
||||
{file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"},
|
||||
{file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"},
|
||||
{file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"},
|
||||
{file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"},
|
||||
{file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"},
|
||||
{file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"},
|
||||
{file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"},
|
||||
{file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"},
|
||||
{file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"},
|
||||
{file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"},
|
||||
{file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"},
|
||||
{file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"},
|
||||
{file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"},
|
||||
{file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -126,17 +144,16 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
requires_python = ">=3.7"
|
||||
version = "8.3.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "Composable command line interface toolkit"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"colorama; platform_system == \"Windows\"",
|
||||
"importlib-metadata; python_version < \"3.8\"",
|
||||
]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
||||
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -144,13 +161,24 @@ name = "colorama"
|
||||
version = "0.4.6"
|
||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
summary = "Cross-platform colored terminal text."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
marker = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser-fork"
|
||||
version = "0.0.14"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Parse Python docstrings in reST, Google and Numpydoc format"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "docstring_parser_fork-0.0.14-py3-none-any.whl", hash = "sha256:4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af"},
|
||||
{file = "docstring_parser_fork-0.0.14.tar.gz", hash = "sha256:a2743a63d8d36c09650594f7b4ab5b2758fee8629dcf794d1b221b23179baa5c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -164,13 +192,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
requires_python = ">=3.8.0"
|
||||
version = "7.0.0"
|
||||
requires_python = ">=3.10.0"
|
||||
summary = "A Python utility / library to sort Python imports."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
{file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"},
|
||||
{file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"mdurl~=0.1",
|
||||
]
|
||||
files = [
|
||||
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
|
||||
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Markdown URL utilities"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -178,7 +231,7 @@ name = "mypy"
|
||||
version = "1.13.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Optional static typing for Python"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"mypy-extensions>=1.0.0",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
@@ -211,13 +264,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
requires_python = ">=3.5"
|
||||
version = "1.1.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Type system extensions for programs checked with the mypy type checker."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -256,13 +309,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
version = "25.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Core utilities for Python packages"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -270,7 +323,7 @@ name = "pathspec"
|
||||
version = "0.12.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Utility library for gitignore style pattern matching of file paths."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
@@ -278,13 +331,89 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
requires_python = ">=3.8"
|
||||
version = "4.5.0"
|
||||
requires_python = ">=3.10"
|
||||
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydoclint"
|
||||
version = "0.7.3"
|
||||
requires_python = ">=3.9"
|
||||
summary = "A Python docstring linter that checks arguments, returns, yields, and raises sections"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"click>=8.1.0",
|
||||
"docstring-parser-fork>=0.0.12",
|
||||
"tomli>=2.0.1; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "pydoclint-0.7.3-py3-none-any.whl", hash = "sha256:a656b0e863565644670ded19a4b506450364e4f1f5e8ff7705d6ba8bb5a82982"},
|
||||
{file = "pydoclint-0.7.3.tar.gz", hash = "sha256:3351d5eeb19f8831d992714f71f5ea1175af649503d39b9da0071445a4002138"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Pygments is a syntax highlighting package written in Python."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytokens"
|
||||
version = "0.2.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"},
|
||||
{file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
requires_python = ">=3.8"
|
||||
summary = "YAML parser and emitter for Python"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -304,16 +433,110 @@ files = [
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
requires_python = ">=3.8.0"
|
||||
summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
groups = ["dev"]
|
||||
dependencies = [
|
||||
"markdown-it-py>=2.2.0",
|
||||
"pygments<3.0.0,>=2.13.0",
|
||||
]
|
||||
files = [
|
||||
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
|
||||
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.1"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b"},
|
||||
{file = "ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224"},
|
||||
{file = "ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151"},
|
||||
{file = "ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5"},
|
||||
{file = "ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d"},
|
||||
{file = "ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6"},
|
||||
{file = "ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1"},
|
||||
{file = "ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44"},
|
||||
{file = "ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
version = "5.5.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Manage dynamic plugins for Python applications"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"},
|
||||
{file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A lil' TOML parser"
|
||||
groups = ["default"]
|
||||
groups = ["dev"]
|
||||
marker = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
|
||||
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
|
||||
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
|
||||
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -332,13 +555,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
groups = ["default"]
|
||||
version = "4.15.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+63
-6
@@ -1,12 +1,23 @@
|
||||
[project]
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.10,<3.13" # in order to support numpy<2.0.0
|
||||
dependencies = [
|
||||
"requests>=2.18.4",
|
||||
"types-requests>=2.31.0.5",
|
||||
"numpy<2.0.0",
|
||||
"black==24.1.0",
|
||||
"isort==5.13.2",
|
||||
"numpy>=1.21.0,<2.0.0",
|
||||
]
|
||||
|
||||
# these will not be included in build distributions
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
## dev dependencies
|
||||
"black==25.9.0",
|
||||
"isort==7.0.0",
|
||||
"mypy==1.13.0",
|
||||
|
||||
## I would like to enable these in the future
|
||||
"bandit>=1.8.2", # code is currently invalid for bandit
|
||||
"ruff>=0.14.1", # code is currently invalid for ruff
|
||||
"pydoclint>=0.7.3", # code is currently invalid for pydoclint
|
||||
]
|
||||
|
||||
[tool.isort]
|
||||
@@ -23,11 +34,50 @@ exclude = '''
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["lib", "out", "addon_updater.py", "addon_updater_ops.py"]
|
||||
target-version = "py310"
|
||||
line-length = 120
|
||||
include = ["pyproject.toml",
|
||||
"**/*.py",
|
||||
"*.py"
|
||||
]
|
||||
exclude = [
|
||||
"lib",
|
||||
"out",
|
||||
"addon_updater.py",
|
||||
"addon_updater_ops.py",
|
||||
"_debug/**",
|
||||
"_bck/**",
|
||||
"*.tmp",
|
||||
"__pycache__/**",
|
||||
".venv/**",
|
||||
"*.pyi",
|
||||
"sentry_sdk",
|
||||
]
|
||||
ignore = [
|
||||
"E501", # Line too long
|
||||
]
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.format]
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["F", "E", "W", "C90", "I", "N", "D", "UP", "S1", "S2", "S3", "S5", "BLE", "FBT",
|
||||
"B", "A", "C4", "COM", "DTZ", "T10", "FA", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20", "Q", "RSE", "RET", "SLF",
|
||||
"SLOT", "SIM", "TID",
|
||||
"TC001", "TC004", "TC005", "TC010",
|
||||
"INT", "ARG", "FIX", "ERA", "PL", "TRY", "FLY", "PERF", "RUF"
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/
|
||||
"D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/
|
||||
"PLC0415", # https://docs.astral.sh/ruff/rules/import-outside-top-level/
|
||||
"RET504", # https://docs.astral.sh/ruff/rules/unnecessary-assign/
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
exclude = ['test_*', 'out', 'lib']
|
||||
@@ -46,3 +96,10 @@ module = [
|
||||
]
|
||||
ignore_missing_imports = true # Ignore missing type hints for bpy
|
||||
|
||||
[tool.pydoclint]
|
||||
style = "google"
|
||||
arg-type-hints-in-docstring = false
|
||||
check-return-types = false
|
||||
ignore-private-args = true
|
||||
ignore-underscore-args = true
|
||||
exclude = "\\.venv|\\sentry_sdk"
|
||||
|
||||
+8
-1
@@ -21,6 +21,7 @@ from logging import getLogger
|
||||
from os.path import basename
|
||||
from re import search
|
||||
from time import time
|
||||
from typing import Literal
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -32,7 +33,12 @@ reports = []
|
||||
|
||||
|
||||
# check for same reports and just make them longer by the timeout.
|
||||
def add_report(text="", timeout=-1, type="INFO", details=""):
|
||||
def add_report(
|
||||
text: str = "",
|
||||
timeout: float = -1,
|
||||
type: Literal["INFO", "ERROR", "VALIDATOR"] = "INFO",
|
||||
details: str = "",
|
||||
) -> None:
|
||||
"""Add text report to GUI. Function checks for same reports and make them longer by the timeout.
|
||||
Also log the text and details into the console with levels: ERROR=RED, INFO=GREEN, VALIDATOR=BLUE.
|
||||
When timeout is not specified, default 15s will be used for ERROR, 5s for INFO/VALIDATOR.
|
||||
@@ -41,6 +47,7 @@ def add_report(text="", timeout=-1, type="INFO", details=""):
|
||||
text = text.strip()
|
||||
full_message = text
|
||||
details = details.strip()
|
||||
color = colors.GRAY
|
||||
if details != "":
|
||||
full_message = f"{text} {details}"
|
||||
|
||||
|
||||
+108
-6
@@ -38,6 +38,7 @@ from . import (
|
||||
client_tasks,
|
||||
comments_utils,
|
||||
datas,
|
||||
download,
|
||||
global_vars,
|
||||
image_utils,
|
||||
paths,
|
||||
@@ -166,6 +167,8 @@ def check_clipboard():
|
||||
target_asset_type = "PRINTABLE"
|
||||
elif asset_type_string.find("nodegroup") > -1:
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("addon") > -1 or asset_type_string.find("add-on") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.asset_type = target_asset_type # switch asset type before placing keywords, so it does not search under wrong asset type
|
||||
@@ -177,7 +180,8 @@ def check_clipboard():
|
||||
# TODO: type annotate and check this crazy function!
|
||||
# Are we sure it behaves correctly on network issues, malfunctioning search etc?
|
||||
def parse_result(r) -> dict:
|
||||
"""Needed to generate some extra data in the result(by now)
|
||||
"""Parse search result into an asset_data by tweaking some of its parameters.
|
||||
We need to generate some extra data in the result (for now).
|
||||
Parameters
|
||||
----------
|
||||
r - search result, also called asset_data
|
||||
@@ -192,10 +196,6 @@ def parse_result(r) -> dict:
|
||||
utils.p("asset with no files-size")
|
||||
|
||||
asset_type = r["assetType"]
|
||||
# TODO remove this condition so all assets are parsed?
|
||||
if len(r["files"]) == 0:
|
||||
return {}
|
||||
|
||||
adata = r["author"]
|
||||
social_networks = datas.parse_social_networks(adata.pop("socialNetworks", []))
|
||||
author = datas.UserProfile(**adata, socialNetworks=social_networks)
|
||||
@@ -389,6 +389,20 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
if comments is None:
|
||||
client_lib.get_comments(asset_data["assetBaseId"])
|
||||
|
||||
# Apply addon-specific status checking and filtering if needed
|
||||
if ui_props.asset_type == "ADDON":
|
||||
# Always process addon search results to store installation status
|
||||
result_field = filter_addon_search_results(
|
||||
result_field, filter_installed_only=False
|
||||
)
|
||||
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
if addon_props.search_installed:
|
||||
# Filter to only show installed addons
|
||||
result_field = [
|
||||
asset for asset in result_field if asset.get("downloaded", 0) > 0
|
||||
]
|
||||
|
||||
# Store results in history step
|
||||
history_step["search_results"] = result_field
|
||||
history_step["search_results_orig"] = task.result
|
||||
@@ -769,7 +783,11 @@ def decide_ordering(query: dict) -> list:
|
||||
# for validators, sort uploaded from oldest
|
||||
order.append("last_blend_upload")
|
||||
else:
|
||||
order.append("-last_blend_upload")
|
||||
if query.get("asset_type") == "addon":
|
||||
# addons don't have athe blend so need to sort by created
|
||||
order.append("-created")
|
||||
else:
|
||||
order.append("-last_blend_upload")
|
||||
elif (
|
||||
query.get("author_id") is not None
|
||||
or query.get("query", "").find("+author_id:") > -1
|
||||
@@ -929,6 +947,68 @@ def build_query_nodegroup(
|
||||
return build_query_common(query, props, ui_props)
|
||||
|
||||
|
||||
def build_query_addon(props, ui_props) -> dict:
|
||||
"""Pure function to construct search query dict for addons."""
|
||||
query = {"asset_type": "addon"}
|
||||
return build_query_common(query, props, ui_props)
|
||||
|
||||
|
||||
def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
"""
|
||||
Filter addon search results based on local installation status.
|
||||
This is called after search results arrive since installation info isn't stored on server.
|
||||
Also stores installation and enablement status in the search results data.
|
||||
|
||||
Args:
|
||||
search_results: List of addon asset data from search
|
||||
filter_installed_only: If True, only return installed addons
|
||||
|
||||
Returns:
|
||||
Filtered list of add-on assets with installation status stored
|
||||
"""
|
||||
|
||||
filtered_results = []
|
||||
|
||||
for asset in search_results:
|
||||
if asset.get("assetType") != "addon":
|
||||
# Skip non-addon assets (shouldn't happen in addon search but safety check)
|
||||
if not filter_installed_only:
|
||||
filtered_results.append(asset)
|
||||
continue
|
||||
|
||||
# Check installation and enablement status for addon
|
||||
try:
|
||||
status = download.get_addon_installation_status(asset)
|
||||
is_installed = status.get("installed", False)
|
||||
is_enabled = status.get("enabled", False)
|
||||
|
||||
# Store installation status in asset data using existing 'downloaded' field
|
||||
# Use 100 for installed, 0 for not installed (matching existing pattern)
|
||||
asset["downloaded"] = 100 if is_installed else 0
|
||||
|
||||
# Store enablement status in new 'enabled' field
|
||||
asset["enabled"] = is_enabled
|
||||
|
||||
if filter_installed_only:
|
||||
if is_installed:
|
||||
filtered_results.append(asset)
|
||||
else:
|
||||
filtered_results.append(asset)
|
||||
|
||||
except Exception as e:
|
||||
# If we can't determine status, mark as not installed/enabled
|
||||
bk_logger.warning(
|
||||
f"Could not determine installation status for addon {asset.get('name', 'Unknown')}: {e}"
|
||||
)
|
||||
asset["downloaded"] = 0
|
||||
asset["enabled"] = False
|
||||
|
||||
if not filter_installed_only:
|
||||
filtered_results.append(asset)
|
||||
|
||||
return filtered_results
|
||||
|
||||
|
||||
def add_search_process(
|
||||
query, get_next: bool, page_size: int, next_url: str, history_id: str
|
||||
):
|
||||
@@ -1106,6 +1186,12 @@ def search(get_next=False, query=None, author_id=""):
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
query = build_query_addon(
|
||||
props=bpy.context.window_manager.blenderkit_addon,
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
# crop long searches
|
||||
if query.get("query"):
|
||||
if len(query["query"]) > 50:
|
||||
@@ -1232,6 +1318,8 @@ def update_filters():
|
||||
sprops.use_filters = sprops.true_hdr
|
||||
elif ui_props.asset_type == "NODEGROUP":
|
||||
sprops.use_filters = fcommon
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
sprops.use_filters = fcommon
|
||||
return True
|
||||
|
||||
|
||||
@@ -1280,6 +1368,9 @@ def detect_asset_type_from_keywords(keywords: str) -> tuple[str, str]:
|
||||
"nodegroup": "NODEGROUP",
|
||||
"node": "NODEGROUP",
|
||||
"printable": "PRINTABLE",
|
||||
"addon": "ADDON",
|
||||
"add-on": "ADDON",
|
||||
"extension": "ADDON",
|
||||
}
|
||||
|
||||
# Convert to lowercase for matching
|
||||
@@ -1357,6 +1448,8 @@ def search_update(self, context):
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("printable") > -1:
|
||||
target_asset_type = "PRINTABLE"
|
||||
elif asset_type_string.find("addon") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.search_keywords = ""
|
||||
@@ -1655,6 +1748,8 @@ def get_ui_state():
|
||||
store_props = store_scene_props
|
||||
elif asset_type == "PRINTABLE":
|
||||
store_props = store_model_props
|
||||
elif asset_type == "ADDON":
|
||||
store_props = [] # Addons don't need to store specific props
|
||||
|
||||
search_props = utils.get_search_props()
|
||||
|
||||
@@ -1664,6 +1759,13 @@ def get_ui_state():
|
||||
if prop_name != "rna_type":
|
||||
ui_state["search_props"][prop_name] = getattr(search_props, prop_name)
|
||||
|
||||
# Store addon-specific search properties
|
||||
if ui_props.asset_type == "ADDON":
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
ui_state["addon_props"] = {
|
||||
"search_installed": addon_props.search_installed,
|
||||
}
|
||||
|
||||
return ui_state
|
||||
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
.updater_install_popup
|
||||
.updater_check_now
|
||||
.updater_update_now
|
||||
.updater_update_target
|
||||
.updater_install_manually
|
||||
.updater_update_successful
|
||||
.updater_restore_backup
|
||||
.updater_ignore
|
||||
.end_background_check
|
||||
view3d.asset_drag_drop
|
||||
object.blenderkit_auto_tags
|
||||
object.blenderkit_generate_thumbnail
|
||||
object.blenderkit_regenerate_thumbnail
|
||||
object.blenderkit_generate_material_thumbnail
|
||||
object.blenderkit_regenerate_material_thumbnail
|
||||
object.kill_bg_process
|
||||
wm.blenderkit_login
|
||||
wm.blenderkit_logout
|
||||
wm.blenderkit_login_cancel
|
||||
scene.blenderkit_addon_manager
|
||||
scene.blenderkit_addon_choice
|
||||
scene.blenderkit_download_kill
|
||||
scene.blenderkit_download
|
||||
wm.blenderkit_bookmark_asset
|
||||
wm.blenderkit_mark_notification_read
|
||||
wm.blenderkit_mark_notifications_read_all
|
||||
wm.blenderkit_open_notification_target
|
||||
wm.blenderkit_upvote_comment
|
||||
wm.blenderkit_is_private_comment
|
||||
wm.blenderkit_post_comment
|
||||
wm.logo_status
|
||||
wm.show_notifications
|
||||
wm.blenderkit_join_discord
|
||||
wm.blenderkit_welcome
|
||||
wm.blenderkit_open_system_directory
|
||||
wm.blenderkit_asset_popup
|
||||
view3d.blenderkit_set_comment_reply_id
|
||||
view3d.blenderkit_set_category_origin
|
||||
view3d.blenderkit_clear_search_keywords
|
||||
view3d.close_popup_button
|
||||
wm.blenderkit_popup_dialog
|
||||
wm.blenderkit_url_dialog
|
||||
wm.blenderkit_login_dialog
|
||||
wm.blenderkit_nodegroup_drop_dialog
|
||||
object.blenderkit_particles_drop
|
||||
object.blenderkit_data_trasnfer
|
||||
wm.modal_timer_operator
|
||||
view3d.run_assetbar_start_modal
|
||||
view3d.run_assetbar_fix_context
|
||||
wm.blenderkit_fast_metadata
|
||||
+1
-1
@@ -225,7 +225,7 @@ class ParticlesDropDialog(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
message = (
|
||||
"This asset is a particle setup. BlenderKit can apply particles to the active/drag-drop object."
|
||||
"The number of particles is caluclated automatically, but if there are too many particles,"
|
||||
"The number of particles is calculated automatically, but if there are too many particles,"
|
||||
" BlenderKit can do the following steps to make sure Blender continues to run:\n"
|
||||
"\n1.Switch to bounding box view of the particles."
|
||||
"\n2.Turn down number of particles that are shown in the view."
|
||||
|
||||
+261
-26
@@ -16,16 +16,161 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import blf
|
||||
import gpu
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
from bpy import app
|
||||
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .image_utils import IMG
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
cached_images = {}
|
||||
|
||||
cached_gpu_textures = {}
|
||||
|
||||
_cached_image_shader: Optional[gpu.types.GPUShader] = None
|
||||
|
||||
|
||||
VERTEX_SHADER_LEGACY = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
in vec2 pos;
|
||||
in vec2 texCoord;
|
||||
out vec2 uv;
|
||||
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
FRAGMENT_SHADER_LEGACY = """
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
uniform sampler2D image;
|
||||
uniform float transparency;
|
||||
uniform int color_space_mode;
|
||||
|
||||
vec3 linear_to_srgb(vec3 linear_color)
|
||||
{
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = linear_color * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(linear_color, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
return mix(lower, higher, step(cutoff, linear_color));
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
color.rgb = linear_to_srgb(color.rgb);
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def create_image_shader_info():
|
||||
"""Return GPU shader info for the runtime image shader."""
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.vertex_in(0, "VEC2", "pos")
|
||||
shader_info.vertex_in(1, "VEC2", "texCoord")
|
||||
|
||||
stage_iface = gpu.types.GPUStageInterfaceInfo("uv_iface")
|
||||
stage_iface.smooth("VEC2", "uv")
|
||||
shader_info.vertex_out(stage_iface)
|
||||
|
||||
shader_info.push_constant("MAT4", "ModelViewProjectionMatrix")
|
||||
shader_info.push_constant("FLOAT", "transparency")
|
||||
shader_info.push_constant("INT", "color_space_mode")
|
||||
shader_info.sampler(0, "FLOAT_2D", "image")
|
||||
|
||||
shader_info.fragment_out(0, "VEC4", "fragColor")
|
||||
shader_info.vertex_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
uv = texCoord;
|
||||
gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
)
|
||||
shader_info.fragment_source(
|
||||
"""
|
||||
void main()
|
||||
{
|
||||
vec4 color = texture(image, uv);
|
||||
if (color_space_mode == 1) {
|
||||
vec3 cutoff = vec3(0.0031308);
|
||||
vec3 lower = color.rgb * 12.92;
|
||||
vec3 higher = 1.055 * pow(max(color.rgb, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;
|
||||
color.rgb = mix(lower, higher, step(cutoff, color.rgb));
|
||||
}
|
||||
color.a *= transparency;
|
||||
fragColor = color;
|
||||
}
|
||||
"""
|
||||
)
|
||||
return shader_info
|
||||
|
||||
|
||||
def create_image_shader():
|
||||
"""Return a cached shader that supports transparency across Blender versions.
|
||||
Features:
|
||||
- sRGB conversion for UI overlays
|
||||
- transparency
|
||||
"""
|
||||
global _cached_image_shader
|
||||
|
||||
if _cached_image_shader is not None:
|
||||
return _cached_image_shader
|
||||
|
||||
shader = None
|
||||
|
||||
create_info_supported = (
|
||||
hasattr(gpu, "shader")
|
||||
and hasattr(gpu.shader, "create_from_info")
|
||||
and hasattr(gpu.types, "GPUShaderCreateInfo")
|
||||
)
|
||||
|
||||
if create_info_supported:
|
||||
try:
|
||||
shader_info = create_image_shader_info()
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
shader = None
|
||||
|
||||
if shader is None:
|
||||
try:
|
||||
shader = gpu.types.GPUShader(VERTEX_SHADER_LEGACY, FRAGMENT_SHADER_LEGACY)
|
||||
except Exception: # noqa: BLE001
|
||||
bk_logger.exception("Failed to create image shader")
|
||||
|
||||
if shader is None:
|
||||
# fallback to builtin shader
|
||||
# mainly for MacOS builds that have issues with custom shaders
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("IMAGE")
|
||||
|
||||
_cached_image_shader = shader
|
||||
return shader
|
||||
|
||||
|
||||
def draw_rect(x, y, width, height, color):
|
||||
"""Used for drawing 2D rectangle backgrounds."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
points = (
|
||||
@@ -48,6 +193,31 @@ def draw_rect(x, y, width, height, color):
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_rect_outline(x, y, width, height, color, line_width=1.0):
|
||||
"""Used for drawing 2D rectangle outlines."""
|
||||
xmax = x + width
|
||||
ymax = y + height
|
||||
coords = (
|
||||
(x, y), # (x, y)
|
||||
(x, ymax), # (x, y)
|
||||
(xmax, ymax), # (x, y)
|
||||
(xmax, y), # (x, y)
|
||||
)
|
||||
indices = ((0, 1), (1, 2), (2, 3), (3, 0))
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": coords}, indices=indices)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(line_width)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_line2d(x1, y1, x2, y2, width, color):
|
||||
"""Used for drawing line from dragged thumbnail to the 3D bounding box."""
|
||||
coords = ((x1, y1), (x2, y2))
|
||||
@@ -115,30 +285,59 @@ def draw_lines(vertices, indices, color):
|
||||
|
||||
|
||||
def draw_rect_3d(coords, color):
|
||||
"""Used for drawing 3D rectangle backgrounds."""
|
||||
indices = [(0, 1, 2), (2, 3, 0)]
|
||||
if app.version < (4, 0, 0):
|
||||
shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
else:
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
cached_images = {}
|
||||
def _resolve_color_space_mode() -> int:
|
||||
"""Return shader color conversion mode for the current drawing context.
|
||||
|
||||
area over non-3D means UI overlay, so we need to apply sRGB conversion."""
|
||||
area = getattr(bpy.context, "area", None)
|
||||
if area is None:
|
||||
return 0
|
||||
|
||||
# Blender 5.0+ node editors already expect linear data, so avoid extra conversion there
|
||||
node_editor_types = {"NODE_EDITOR", "VIEW_3D"}
|
||||
|
||||
if area.type in node_editor_types:
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batch=None):
|
||||
# draw_rect(x,y, width, height, (.5,0,0,.5))
|
||||
def draw_image_runtime(
|
||||
x: float,
|
||||
y: float,
|
||||
width: float,
|
||||
height: float,
|
||||
image: Union[bpy.types.Image, IMG],
|
||||
transparency: Optional[float] = 1.0,
|
||||
crop: Tuple[float, float, float, float] = (0, 0, 1, 1),
|
||||
batch: Optional[gpu.types.GPUBatch] = None,
|
||||
) -> Optional[gpu.types.GPUBatch]:
|
||||
"""Draws an image at given location with given size.
|
||||
|
||||
try:
|
||||
image.name
|
||||
except:
|
||||
print("Image is invalid- draw function")
|
||||
return
|
||||
Returns:
|
||||
The batch object if successful, or None if the image is invalid.
|
||||
"""
|
||||
if not image.name or not image.filepath:
|
||||
return None
|
||||
|
||||
ci = cached_images.get(image.filepath)
|
||||
image_shader = create_image_shader()
|
||||
|
||||
texture = None
|
||||
ci = cached_images.get(image.filepath + "GPU_TEXTURE")
|
||||
if ci is not None:
|
||||
if (
|
||||
ci["x"] == x
|
||||
@@ -149,6 +348,7 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
batch = ci["batch"]
|
||||
image_shader = ci["image_shader"]
|
||||
texture = ci["texture"]
|
||||
|
||||
if not batch:
|
||||
coords = [(x, y), (x + width, y), (x, y + height), (x + width, y + height)]
|
||||
|
||||
@@ -161,16 +361,14 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
|
||||
indices = [(0, 1, 2), (2, 1, 3)]
|
||||
|
||||
if app.version < (4, 0, 0):
|
||||
image_shader = gpu.shader.from_builtin("2D_IMAGE")
|
||||
else:
|
||||
image_shader = gpu.shader.from_builtin("IMAGE")
|
||||
batch = batch_for_shader(
|
||||
image_shader, "TRIS", {"pos": coords, "texCoord": uvs}, indices=indices
|
||||
)
|
||||
texture = gpu.texture.from_image(image)
|
||||
|
||||
texture = path_to_gpu_texture(image.filepath)
|
||||
|
||||
# tell shader to use the image that is bound to image unit 0
|
||||
cached_images[image.filepath] = {
|
||||
cached_images[image.filepath + "GPU_TEXTURE"] = {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": width,
|
||||
@@ -179,19 +377,56 @@ def draw_image(x, y, width, height, image, transparency, crop=(0, 0, 1, 1), batc
|
||||
"image_shader": image_shader,
|
||||
"texture": texture,
|
||||
}
|
||||
# send image to gpu if it isn't there already
|
||||
if image.gl_load():
|
||||
raise Exception()
|
||||
|
||||
# texture = gpu.texture.from_image(image)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
batch.draw(image_shader)
|
||||
if batch is None:
|
||||
return None
|
||||
|
||||
if image_shader and texture:
|
||||
color_space_mode = _resolve_color_space_mode()
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
image_shader.bind()
|
||||
image_shader.uniform_sampler("image", texture)
|
||||
|
||||
# may not be available in simple shader
|
||||
try:
|
||||
# set floats
|
||||
image_shader.uniform_float("transparency", transparency)
|
||||
|
||||
# set color space mode
|
||||
image_shader.uniform_int("color_space_mode", color_space_mode)
|
||||
batch.draw(image_shader)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
def path_to_gpu_texture(path: str) -> Optional[gpu.types.GPUTexture]:
|
||||
"""Convert a Blender image to a GPU texture.
|
||||
|
||||
Returns:
|
||||
The GPU texture if successful, or None if the image is invalid.
|
||||
"""
|
||||
# check if exists and is file [prevent exception for missing files]
|
||||
if path in cached_gpu_textures:
|
||||
return cached_gpu_textures[path]
|
||||
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
# do not spam log with warnings, just return None
|
||||
return None
|
||||
img = bpy.data.images.load(path, check_existing=False)
|
||||
img.gl_load()
|
||||
|
||||
tex = gpu.texture.from_image(img)
|
||||
cached_gpu_textures[path] = tex
|
||||
|
||||
# # Clean up Blender image
|
||||
bpy.data.images.remove(img)
|
||||
return tex
|
||||
|
||||
|
||||
def get_text_size(font_id=0, text="", text_size=16, dpi=72):
|
||||
if app.version < (4, 0, 0):
|
||||
blf.size(font_id, text_size, dpi)
|
||||
|
||||
+232
-67
@@ -16,10 +16,8 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import time
|
||||
from webbrowser import open_new_tab
|
||||
@@ -87,6 +85,9 @@ def draw_upload_common(layout, props, asset_type, context):
|
||||
url = (
|
||||
paths.BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL
|
||||
) # Reuse model instructions since prints are similar
|
||||
if asset_type == "ADDON":
|
||||
asset_type_text = asset_type
|
||||
url = paths.BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL
|
||||
op = layout.operator(
|
||||
"wm.url_open", text=f"Read {asset_type} upload instructions", icon="QUESTION"
|
||||
)
|
||||
@@ -225,6 +226,19 @@ def draw_panel_hdr_search(self, context):
|
||||
utils.label_multiline(layout, text=props.report)
|
||||
|
||||
|
||||
def draw_panel_addon_search(self, context):
|
||||
wm = context.window_manager
|
||||
ui_props = wm.blenderkitUI
|
||||
addon_props = wm.blenderkit_addon
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.prop(ui_props, "search_keywords", text="", icon="VIEWZOOM")
|
||||
draw_assetbar_show_hide(row, addon_props)
|
||||
|
||||
utils.label_multiline(layout, text=addon_props.report)
|
||||
|
||||
|
||||
def draw_panel_nodegroup_upload(self, context):
|
||||
layout = self.layout
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -458,6 +472,8 @@ def draw_model_context_menu(self, context):
|
||||
layout = self.layout
|
||||
|
||||
o = utils.get_active_model()
|
||||
if not o:
|
||||
return
|
||||
if o.get("asset_data") is None:
|
||||
utils.label_multiline(
|
||||
layout,
|
||||
@@ -1563,6 +1579,32 @@ class VIEW3D_PT_blenderkit_advanced_nodegroup_search(Panel):
|
||||
draw_common_filters(self.layout, ui_props)
|
||||
|
||||
|
||||
class VIEW3D_PT_blenderkit_advanced_addon_search(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_advanced_addon_search"
|
||||
bl_parent_id = "VIEW3D_PT_blenderkit_unified"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_label = "Search filters"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if not global_vars.CLIENT_RUNNING:
|
||||
return False
|
||||
return ui_props.down_up == "SEARCH" and ui_props.asset_type == "ADDON"
|
||||
|
||||
def draw(self, context):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
draw_common_filters(self.layout, ui_props)
|
||||
layout = self.layout
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
# Add installed filter for addons
|
||||
row = layout.row()
|
||||
row.prop(addon_props, "search_installed", text="Installed Only")
|
||||
|
||||
|
||||
class VIEW3D_PT_blenderkit_advanced_printable_search(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_advanced_printable_search"
|
||||
@@ -1819,6 +1861,9 @@ class VIEW3D_PT_blenderkit_unified(Panel):
|
||||
if ui_props.asset_type == "NODEGROUP":
|
||||
return draw_panel_nodegroup_search(self, context)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
return draw_panel_addon_search(self, context)
|
||||
|
||||
def draw_upload(self, context, layout, ui_props):
|
||||
obj = utils.get_active_asset()
|
||||
props = getattr(obj, "blenderkit", None)
|
||||
@@ -1861,6 +1906,15 @@ class VIEW3D_PT_blenderkit_unified(Panel):
|
||||
if ui_props.asset_type == "NODEGROUP":
|
||||
return draw_panel_nodegroup_upload(self, context)
|
||||
|
||||
if ui_props.asset_type == "ADDON":
|
||||
layout.label(text="Add-on uploads are managed through")
|
||||
layout.label(text="the BlenderKit website.")
|
||||
op = layout.operator(
|
||||
"wm.url_open", text="Go to BlenderKit Website", icon="URL"
|
||||
)
|
||||
op.url = paths.BLENDERKIT_ADDON_UPLOAD_INSTRUCTIONS_URL
|
||||
return
|
||||
|
||||
|
||||
class BlenderKitWelcomeOperator(bpy.types.Operator):
|
||||
"""Login online on BlenderKit webpage"""
|
||||
@@ -2125,10 +2179,11 @@ def draw_asset_context_menu(
|
||||
op.asset_base_id = asset_data["assetBaseId"]
|
||||
if asset_data["assetType"] == "model":
|
||||
o = utils.get_active_model()
|
||||
op.model_location = o.location
|
||||
op.model_rotation = o.rotation_euler
|
||||
op.target_object = o.name
|
||||
op.material_target_slot = o.active_material_index
|
||||
if o is not None:
|
||||
op.model_location = o.location
|
||||
op.model_rotation = o.rotation_euler
|
||||
op.target_object = o.name
|
||||
op.material_target_slot = o.active_material_index
|
||||
|
||||
elif asset_data["assetType"] == "material":
|
||||
aob = bpy.context.active_object
|
||||
@@ -2438,8 +2493,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
icon=icon,
|
||||
emboss=emboss,
|
||||
)
|
||||
# additional questionmark icon where it's important?
|
||||
# Embossed elements are visibly clickable, so we don't need the questionmark icon
|
||||
# additional 'question mark' icon where it's important?
|
||||
# Embossed elements are visibly clickable, so we don't need the 'question mark' icon
|
||||
if url != "" and not emboss:
|
||||
split = split.split()
|
||||
op = split.operator("wm.blenderkit_url", text="", icon="QUESTION")
|
||||
@@ -2512,15 +2567,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
box.separator()
|
||||
|
||||
if self.asset_data.get("license") == "cc_zero":
|
||||
t = "CC Zero "
|
||||
text = "CC Zero "
|
||||
icon = pcoll["cc0"]
|
||||
else:
|
||||
t = "Royalty free"
|
||||
text = "Royalty free"
|
||||
icon = pcoll["royalty_free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"License",
|
||||
t,
|
||||
text,
|
||||
# icon_value=icon.icon_id,
|
||||
url=f"{global_vars.SERVER}/docs/licenses/",
|
||||
tooltip="All BlenderKit assets are available for commercial use. \n"
|
||||
@@ -2614,8 +2669,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
self.draw_asset_parameter(box, key="modelStyle", pretext="Style")
|
||||
|
||||
if utils.get_param(self.asset_data, "dimensionX"):
|
||||
t = utils.fmt_dimensions(mparams)
|
||||
self.draw_property(box, "Size", t)
|
||||
text = utils.fmt_dimensions(mparams)
|
||||
self.draw_property(box, "Size", text)
|
||||
if self.asset_data.get("filesSize"):
|
||||
fs = self.asset_data["filesSize"] * 1024
|
||||
# multiply because the number is reduced when search is done to avoind C intiger limit with large files
|
||||
@@ -2655,38 +2710,147 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
|
||||
# self.draw_property(box, 'Tags', self.asset_data['tags']) #TODO make them clickable!
|
||||
|
||||
# Free/Full plan or private Access
|
||||
# Free/Full plan or private Access - with special handling for addons
|
||||
plans_tooltip = (
|
||||
"BlenderKit has 2 plans:\n"
|
||||
" * Free plan - more than 50% of all assets\n"
|
||||
" * Full plan - unlimited access to everything\n"
|
||||
"Click to go to subscriptions page"
|
||||
)
|
||||
if self.asset_data["isPrivate"]:
|
||||
t = "Private"
|
||||
self.draw_property(box, "Access", t, icon="LOCKED")
|
||||
elif self.asset_data["isFree"]:
|
||||
t = "Free plan"
|
||||
icon = pcoll["free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
t,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
|
||||
# Special pricing display for addons
|
||||
if self.asset_data.get("assetType") == "addon":
|
||||
|
||||
can_download = self.asset_data.get("canDownload")
|
||||
is_free = self.asset_data.get("isFree")
|
||||
|
||||
# Get pricing info from extensions cache
|
||||
base_price = self.asset_data.get("basePrice")
|
||||
is_for_sale = self.asset_data.get("isForSale")
|
||||
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
text = f"${base_price} (Not purchased)"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is for sale but you haven't purchased it yet",
|
||||
)
|
||||
elif is_for_sale and can_download and base_price:
|
||||
text = f"${base_price} (Purchased)"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="You have purchased this addon",
|
||||
)
|
||||
elif not is_free and not is_for_sale:
|
||||
text = "Full plan required"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
else:
|
||||
text = "Free"
|
||||
icon = pcoll["free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is free to use",
|
||||
)
|
||||
|
||||
# Display Blender version requirements for addons
|
||||
dict_params = self.asset_data.get("dictParameters", {})
|
||||
min_version = dict_params.get("blenderVersionMin")
|
||||
max_version = dict_params.get("blenderVersionMax")
|
||||
if min_version:
|
||||
min_version_tuple = tuple(map(int, min_version.split(".")))
|
||||
if max_version:
|
||||
max_version_tuple = tuple(map(int, max_version.split(".")))
|
||||
|
||||
if min_version or max_version:
|
||||
version_text = ""
|
||||
if min_version and max_version:
|
||||
version_text = f"{min_version} - {max_version}"
|
||||
elif min_version:
|
||||
version_text = f"{min_version}+"
|
||||
elif max_version:
|
||||
version_text = f"≤ {max_version}"
|
||||
|
||||
# Check if current Blender version is compatible
|
||||
current_version = (
|
||||
f"{bpy.app.version[0]}.{bpy.app.version[1]}.{bpy.app.version[2]}"
|
||||
)
|
||||
is_compatible = True
|
||||
|
||||
if min_version:
|
||||
if bpy.app.version < min_version_tuple:
|
||||
is_compatible = False
|
||||
|
||||
if max_version and is_compatible:
|
||||
if bpy.app.version > max_version_tuple:
|
||||
is_compatible = False
|
||||
|
||||
# Display version requirement with appropriate warning
|
||||
if not is_compatible:
|
||||
box.alert = True
|
||||
self.draw_property(
|
||||
box,
|
||||
"Blender versions",
|
||||
f"{version_text} (Incompatible!)",
|
||||
icon="ERROR",
|
||||
tooltip=f"This addon requires Blender {version_text}, but you're using {current_version}",
|
||||
)
|
||||
box.alert = False
|
||||
else:
|
||||
self.draw_property(
|
||||
box,
|
||||
"Blender versions",
|
||||
version_text,
|
||||
icon="CHECKMARK",
|
||||
tooltip=f"This addon is compatible with your Blender version ({current_version})",
|
||||
)
|
||||
else:
|
||||
t = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
t,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
# Regular asset access display
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif self.asset_data["isFree"]:
|
||||
text = "Free plan"
|
||||
icon = pcoll["free"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
else:
|
||||
text = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Access",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip=plans_tooltip,
|
||||
url=paths.BLENDERKIT_PLANS_URL,
|
||||
)
|
||||
|
||||
if utils.profile_is_validator():
|
||||
date = self.asset_data["created"][:10]
|
||||
@@ -2730,7 +2894,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
)
|
||||
|
||||
# Add TwinBru specific parameters for material assets
|
||||
# only if they have twinbruReference in the dictparameters
|
||||
# only if they have 'twinbruReference' in the 'dictParameters'
|
||||
if self.asset_data.get("dictParameters").get("twinbruReference"):
|
||||
box.separator()
|
||||
box.label(text="TwinBru physical material categories")
|
||||
@@ -3220,10 +3384,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
split_ratio = 0.45
|
||||
split_left = row.split(factor=split_ratio)
|
||||
left_column = split_left.column()
|
||||
|
||||
self.draw_thumbnail_box(left_column, width=int(self.width * split_ratio))
|
||||
|
||||
if not utils.user_is_owner(asset_data=self.asset_data):
|
||||
if (
|
||||
not utils.user_is_owner(asset_data=self.asset_data)
|
||||
and self.asset_data.get("assetType") != "addon"
|
||||
):
|
||||
# Draw ratings, but not for owners of assets - doesn't make sense.
|
||||
# also addons are now disabled until we figure out how to handle them.
|
||||
ratings_box = left_column.box()
|
||||
self.prefill_ratings()
|
||||
ratings.draw_ratings_menu(self, context, ratings_box)
|
||||
@@ -3412,35 +3581,18 @@ class ClearSearchKeywords(bpy.types.Operator):
|
||||
|
||||
|
||||
class ClosePopupButton(bpy.types.Operator):
|
||||
"""Close popup window"""
|
||||
"""Close the popup window. It can also be closed by pressing Esc or clicking outside it."""
|
||||
|
||||
bl_idname = "view3d.close_popup_button"
|
||||
bl_label = "Close popup"
|
||||
bl_label = "Close Popup"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def win_close(self):
|
||||
VK_ESCAPE = 0x1B
|
||||
ctypes.windll.user32.keybd_event(VK_ESCAPE)
|
||||
return True
|
||||
|
||||
def mouse_trick(self, context, x, y):
|
||||
# import time
|
||||
context.area.tag_redraw()
|
||||
w = context.window
|
||||
w.cursor_warp(w.x + 15, w.y + w.height - 15)
|
||||
# time.sleep(.12)
|
||||
w.cursor_warp(x, y)
|
||||
context.area.tag_redraw()
|
||||
|
||||
def invoke(self, context, event):
|
||||
if platform.system() == "Windows":
|
||||
self.win_close()
|
||||
else:
|
||||
self.mouse_trick(context, event.mouse_x, event.mouse_y)
|
||||
"""Force the (containing, parent) popup to close.
|
||||
This was done by emulating Esc or hacking mouse, but stopped working in B5.
|
||||
But can be effectively done by just tweaking screen: https://blender.stackexchange.com/a/329900
|
||||
"""
|
||||
bpy.context.window.screen = bpy.context.window.screen
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3492,9 +3644,13 @@ class UrlPopupDialog(bpy.types.Operator):
|
||||
layout.active_default = True
|
||||
op = layout.operator("wm.url_open", text=self.link_text, icon="QUESTION")
|
||||
if not utils.user_logged_in():
|
||||
if self.message.find("purchased") != -1:
|
||||
text = "purchased"
|
||||
else:
|
||||
text = "subscribed"
|
||||
utils.label_multiline(
|
||||
layout,
|
||||
text="Already subscribed? Log in to access your account.",
|
||||
text=f"Already {text}? Log in to access your account.",
|
||||
width=300,
|
||||
)
|
||||
|
||||
@@ -3695,6 +3851,7 @@ def header_search_draw(self, context):
|
||||
"HDR": wm.blenderkit_HDR,
|
||||
"SCENE": wm.blenderkit_scene,
|
||||
"NODEGROUP": wm.blenderkit_nodegroup,
|
||||
"ADDON": wm.blenderkit_addon,
|
||||
}
|
||||
props = props_dict[ui_props.asset_type]
|
||||
pcoll = icons.icon_collections["main"]
|
||||
@@ -3708,6 +3865,7 @@ def header_search_draw(self, context):
|
||||
"HDR": "WORLD",
|
||||
"SCENE": "SCENE_DATA",
|
||||
"NODEGROUP": "NODETREE",
|
||||
"ADDON": "PLUGIN",
|
||||
}
|
||||
|
||||
asset_type_icon = icons_dict[ui_props.asset_type]
|
||||
@@ -3845,6 +4003,12 @@ def header_search_draw(self, context):
|
||||
text="",
|
||||
icon_value=icon_id,
|
||||
)
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
layout.popover(
|
||||
panel="VIEW3D_PT_blenderkit_advanced_addon_search",
|
||||
text="",
|
||||
icon_value=icon_id,
|
||||
)
|
||||
elif ui_props.asset_type == "PRINTABLE":
|
||||
layout.popover(
|
||||
panel="VIEW3D_PT_blenderkit_advanced_printable_search",
|
||||
@@ -4039,7 +4203,7 @@ class NodegroupDropDialog(bpy.types.Operator):
|
||||
# When adding as a node, use node positioning; when adding as modifier, use 3D positioning
|
||||
if self.add_mode == "NODE":
|
||||
bpy.ops.scene.blenderkit_download(
|
||||
True,
|
||||
"EXEC_DEFAULT",
|
||||
asset_index=self.asset_search_index,
|
||||
node_x=self.node_x,
|
||||
node_y=self.node_y,
|
||||
@@ -4050,7 +4214,7 @@ class NodegroupDropDialog(bpy.types.Operator):
|
||||
)
|
||||
else: # MODIFIER mode
|
||||
bpy.ops.scene.blenderkit_download(
|
||||
True,
|
||||
"EXEC_DEFAULT",
|
||||
asset_index=self.asset_search_index,
|
||||
model_location=self.snapped_location,
|
||||
model_rotation=self.snapped_rotation,
|
||||
@@ -4080,6 +4244,7 @@ classes = (
|
||||
VIEW3D_PT_blenderkit_advanced_HDR_search,
|
||||
VIEW3D_PT_blenderkit_advanced_brush_search,
|
||||
VIEW3D_PT_blenderkit_advanced_nodegroup_search,
|
||||
VIEW3D_PT_blenderkit_advanced_addon_search,
|
||||
VIEW3D_PT_blenderkit_advanced_printable_search,
|
||||
VIEW3D_PT_blenderkit_categories,
|
||||
VIEW3D_PT_blenderkit_import_settings,
|
||||
|
||||
+40
-16
@@ -304,6 +304,11 @@ def get_search_props():
|
||||
if not hasattr(wm, "blenderkit_nodegroup"):
|
||||
return
|
||||
props = wm.blenderkit_nodegroup
|
||||
|
||||
if uiprops.asset_type == "ADDON":
|
||||
if not hasattr(wm, "blenderkit_addon"):
|
||||
return
|
||||
props = wm.blenderkit_addon
|
||||
return props
|
||||
|
||||
|
||||
@@ -357,6 +362,8 @@ def get_active_asset():
|
||||
return get_active_brush()
|
||||
elif ui_props.asset_type == "NODEGROUP":
|
||||
return get_active_nodegroup()
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
return None # Addons don't have an active asset concept
|
||||
|
||||
return None
|
||||
|
||||
@@ -394,6 +401,8 @@ def get_upload_props():
|
||||
b = get_active_nodegroup()
|
||||
if b is not None:
|
||||
return b.blenderkit
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
return None # Addons don't have upload props
|
||||
return None
|
||||
|
||||
|
||||
@@ -647,10 +656,11 @@ def img_to_preview(img, copy_original=False):
|
||||
def get_hidden_image(
|
||||
tpath, bdata_name, force_reload: bool = False, colorspace: str = ""
|
||||
):
|
||||
"""Get hidden image by name. If not found, load it from tpath."""
|
||||
if bdata_name[0] == ".":
|
||||
hidden_name = bdata_name
|
||||
else:
|
||||
hidden_name = ".%s" % bdata_name
|
||||
hidden_name = f".{bdata_name}"
|
||||
img = bpy.data.images.get(hidden_name) # type: ignore[union-attr]
|
||||
|
||||
if tpath.startswith("//"):
|
||||
@@ -687,14 +697,14 @@ def get_hidden_image(
|
||||
|
||||
|
||||
def get_thumbnail(name):
|
||||
"""Get addon thumbnail image by name."""
|
||||
p = paths.get_addon_thumbnail_path(name)
|
||||
name = ".%s" % name
|
||||
name = f".{name}"
|
||||
img = bpy.data.images.get(name)
|
||||
if img == None:
|
||||
if img is None:
|
||||
img = bpy.data.images.load(p, check_existing=True)
|
||||
image_utils.set_colorspace(img)
|
||||
img.name = name
|
||||
img.name = name
|
||||
|
||||
return img
|
||||
|
||||
@@ -1224,6 +1234,9 @@ def user_is_owner(asset_data: Optional[dict] = None) -> bool:
|
||||
|
||||
def asset_from_newer_blender_version(asset_data, blender_version=None):
|
||||
"""Check if asset is from a newer blender version, to avoid incompatibility. Give info if difference is in major, minor or patch version."""
|
||||
# addons don't have a blender version, so we return False
|
||||
if asset_data["assetType"] == "addon":
|
||||
return False, ""
|
||||
asset_ver = asset_data["sourceAppVersion"].split(".")
|
||||
if blender_version is None:
|
||||
blender_version = bpy.app.version
|
||||
@@ -1266,27 +1279,38 @@ def guard_from_crash():
|
||||
|
||||
|
||||
def get_largest_area(context=None, area_type="VIEW_3D"):
|
||||
"""Get the largest area of the given type."""
|
||||
maxsurf = 0
|
||||
maxa = None
|
||||
maxw = None
|
||||
region = None
|
||||
if context is None:
|
||||
windows = bpy.data.window_managers[0].windows
|
||||
if bpy.context.window is not None:
|
||||
windows = [bpy.context.window]
|
||||
else:
|
||||
windows = bpy.data.window_managers.windows
|
||||
else:
|
||||
windows = context.window_manager.windows
|
||||
for w in windows:
|
||||
for a in w.screen.areas:
|
||||
if a.type == area_type:
|
||||
asurf = a.width * a.height
|
||||
if asurf > maxsurf:
|
||||
maxa = a
|
||||
maxw = w
|
||||
maxsurf = asurf
|
||||
if bpy.context.area is not None and bpy.context.area.type == area_type:
|
||||
maxa = bpy.context.area
|
||||
maxw = bpy.context.window
|
||||
maxsurf = maxa.width * maxa.height
|
||||
region = maxa.regions[-1]
|
||||
else:
|
||||
areas = w.screen.areas
|
||||
for a in w.screen.areas:
|
||||
if a.type == area_type:
|
||||
asurf = a.width * a.height
|
||||
if asurf > maxsurf:
|
||||
maxa = a
|
||||
maxw = w
|
||||
maxsurf = asurf
|
||||
|
||||
region = a.regions[-1]
|
||||
# for r in a.regions:
|
||||
# if r.type == 'WINDOW':
|
||||
# region = r
|
||||
region = a.regions[-1]
|
||||
# for r in a.regions:
|
||||
# if r.type == 'WINDOW':
|
||||
# region = r
|
||||
|
||||
if maxw is None or maxa is None:
|
||||
return None, None, None
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2025-12-22 09:58:29.267931",
|
||||
"backup_date": "December-1-2025",
|
||||
"last_check": "2026-02-12 10:20:22.908483",
|
||||
"backup_date": "January-12-2026",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
Reference in New Issue
Block a user