2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
+268 -61
View File
@@ -29,10 +29,7 @@ from typing import Optional, Union
import bpy
from bpy.app.handlers import persistent
from bpy.props import ( # TODO only keep the ones actually used when cleaning
BoolProperty,
StringProperty,
)
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from . import (
@@ -41,10 +38,10 @@ from . import (
client_tasks,
comments_utils,
datas,
download,
global_vars,
image_utils,
paths,
ratings_utils,
reports,
resolutions,
tasks_queue,
@@ -170,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
@@ -181,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
@@ -196,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)
@@ -393,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
@@ -709,25 +719,16 @@ def query_to_url(
query = {}
url = f"{paths.BLENDERKIT_API}/search/"
if query is None:
query = {}
requeststring = "?query="
if query.get("query") not in ("", None):
requeststring += urllib.parse.quote_plus(query["query"]) # .lower()
requeststring += urllib.parse.quote_plus(query["query"])
for q in query:
if q != "query" and q != "free_first":
requeststring += (
f"+{q}:{urllib.parse.quote_plus(str(query[q]))}" # .lower()
)
if q in ["query", "free_first", "search_order_by"]:
continue
requeststring += f"+{q}:{urllib.parse.quote_plus(str(query[q]))}"
# add dict_parameters to make results smaller
# result ordering: _score - relevance, score - BlenderKit score
order = []
if query.get("free_first", False):
order = [
"-is_free",
]
# query with category_subtree:model etc gives irrelevant results
if query.get("category_subtree") in (
@@ -741,24 +742,7 @@ def query_to_url(
):
query["category_subtree"] = None
if query.get("query") is None and query.get("category_subtree") == None:
# assumes no keywords and no category, thus an empty search that is triggered on start.
# orders by last core file upload
if query.get("verification_status") == "uploaded":
# for validators, sort uploaded from oldest
order.append("last_blend_upload")
else:
order.append("-last_blend_upload")
elif (
query.get("author_id") is not None
or query.get("query", "").find("+author_id:") > -1
) and utils.profile_is_validator():
order.append("-created")
else:
if query.get("category_subtree") is not None:
order.append("-score,_score")
else:
order.append("_score")
order = decide_ordering(query)
if requeststring.find("+order:") == -1:
requeststring += "+order:" + ",".join(order)
requeststring += "&dict_parameters=1"
@@ -774,6 +758,50 @@ def query_to_url(
return urlquery
def decide_ordering(query: dict) -> list:
"""Decides which ordering should be used based on the search_order_by.
If search_order_by is not default, its value is used for the sorting (quality, uploaded, etc.).
Otherwise the 'legacy' mode is used which
"""
# result ordering: _score - relevance, score - BlenderKit score
order = []
if query.get("free_first", False):
order = [
"-is_free",
]
search_order_by = query.get("search_order_by", "default")
if search_order_by != "default":
order.append(search_order_by)
return order
# DEFAULT TRADITIONAL SMART ORDERING
if query.get("query") is None and query.get("category_subtree") == None:
# assumes no keywords and no category, thus an empty search that is triggered on start.
# orders by last core file upload
if query.get("verification_status") == "uploaded":
# for validators, sort uploaded from oldest
order.append("last_blend_upload")
else:
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
) and utils.profile_is_validator():
order.append("-created")
else:
if query.get("category_subtree") is not None:
order.append("-score,_score")
else:
order.append("_score")
return order
def build_query_common(query: dict, props, ui_props) -> dict:
"""Pure function to add shared parameters based on props to query dict.
Returns the updated version of the query dict.
@@ -919,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
):
@@ -1096,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:
@@ -1113,7 +1209,6 @@ def search(get_next=False, query=None, author_id=""):
if author_id != "":
query["author_id"] = author_id
elif ui_props.own_only:
# if user searches for [another] author, 'only my assets' is invalid. that's why in elif.
profile = global_vars.BKIT_PROFILE
@@ -1122,10 +1217,11 @@ def search(get_next=False, query=None, author_id=""):
# free first has to by in query to be evaluated as changed as another search, otherwise the filter is not updated.
query["free_first"] = ui_props.free_only
query["search_order_by"] = ui_props.search_order_by
active_history_step["is_searching"] = True
page_size = min(40, ui_props.wcount * user_preferences.max_assetbar_rows + 5)
page_size = min(40, ui_props.wcount * user_preferences.maximized_assetbar_rows + 5)
next_url = ""
if get_next and active_history_step.get("search_results_orig"):
@@ -1198,6 +1294,7 @@ def update_filters():
or ui_props.search_bookmarks
or ui_props.search_license != "ANY"
or ui_props.search_blender_version
or ui_props.search_order_by != "default"
# NSFW filter is signaled in a special way and should not affect the filter icon
)
@@ -1221,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
@@ -1269,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
@@ -1300,20 +1402,21 @@ def search_update(self, context):
ui_props = bpy.context.window_manager.blenderkitUI
# Remove this feature for now, but leave the code here for future reference
# Check if keywords contain asset type before processing clipboard
if ui_props.search_keywords != "":
detected_type, cleaned_keywords = detect_asset_type_from_keywords(
ui_props.search_keywords
)
if detected_type and detected_type != ui_props.asset_type:
# Store keywords before switching
ui_props.search_lock = True
ui_props.search_keywords = cleaned_keywords
# Switch asset type
ui_props.asset_type = detected_type
ui_props.search_lock = False
# Return since changing keywords will trigger this function again
# not now - let's try it with lock
# if ui_props.search_keywords != "":
# detected_type, cleaned_keywords = detect_asset_type_from_keywords(
# ui_props.search_keywords
# )
# if detected_type and detected_type != ui_props.asset_type:
# # Store keywords before switching
# ui_props.search_lock = True
# ui_props.search_keywords = cleaned_keywords
# # Switch asset type
# ui_props.asset_type = detected_type
# ui_props.search_lock = False
# Return since changing keywords will trigger this function again
# not now - let's try it with lock
# if ui_props.down_up != "SEARCH":
# ui_props.down_up = "SEARCH"
@@ -1345,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 = ""
@@ -1643,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()
@@ -1652,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
@@ -1695,15 +1809,18 @@ def update_tab_name(active_tab):
# Update tab name
active_tab["name"] = tab_name
# Update UI if asset bar exists
# Update UI if asset bar exists and is properly initialized
asset_bar = asset_bar_op.asset_bar_operator
if asset_bar and hasattr(asset_bar, "tab_buttons"):
active_tab_index = global_vars.TABS["active_tab"]
if 0 <= active_tab_index < len(asset_bar.tab_buttons):
asset_bar.tab_buttons[active_tab_index].text = tab_name
# Force redraw of the region
if asset_bar.area:
asset_bar.area.tag_redraw()
try:
asset_bar.tab_buttons[active_tab_index].text = tab_name
# Only try to redraw if we have a valid region
if asset_bar.area and asset_bar.area.region:
asset_bar.area.tag_redraw()
except Exception as e:
bk_logger.debug(f"Could not update tab name in UI: {e}")
return history_step
@@ -1752,6 +1869,96 @@ def create_history_step(active_tab):
return history_step
def append_history_step(
search_keywords,
search_results,
active_tab=None,
asset_type=None,
search_results_orig=None,
) -> dict:
"""Append a complete history step consisting of search keywords and results. No search is triggered.
Use this function when you already have search results data and want to add them to the history step.
Function also switches the asset type to the one provided, refreshes the UI and updates the tab name.
Parameters
----------
search_keywords : str
The search keywords to use for this history step
search_results : list
List of parsed search results to store in the history step
active_tab : dict
The active tab to add the history step to
asset_type : str, optional
The asset type to use. If None, current asset type will be used
search_results_orig : dict, optional
The original search results from the server. If None, will be constructed from search_results
Returns
-------
dict
The newly created history step
"""
if active_tab is None:
active_tab = get_active_tab()
ui_state = get_ui_state()
ui_state["ui_props"]["search_keywords"] = search_keywords
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.search_lock = True
if asset_type:
ui_state["ui_props"]["asset_type"] = asset_type
ui_props.asset_type = asset_type
ui_props.search_keywords = search_keywords
ui_props.search_lock = False
# Create the history step
history_step = {
"id": str(uuid.uuid4()),
"ui_state": ui_state,
"scroll_offset": 0, # Reset scroll offset for new search
"search_results": search_results,
"is_searching": False,
}
# Add original search results if provided, otherwise construct from search_results
if search_results_orig:
history_step["search_results_orig"] = search_results_orig
else:
history_step["search_results_orig"] = {
"results": search_results,
"count": len(search_results),
}
# Delete any future history steps
if active_tab["history_index"] < len(active_tab["history"]) - 1:
# Remove future steps from global history steps dict first
for step in active_tab["history"][active_tab["history_index"] + 1 :]:
global_vars.DATA["history steps"].pop(step["id"], None)
# Then truncate the tab's history list
active_tab["history"] = active_tab["history"][: active_tab["history_index"] + 1]
# Add to tab history
active_tab["history"].append(history_step)
active_tab["history_index"] = len(active_tab["history"]) - 1
# Add to global history steps
global_vars.DATA["history steps"][history_step["id"]] = history_step
# Update tab name
update_tab_name(active_tab)
# Update history button visibility if asset bar exists
asset_bar = asset_bar_op.asset_bar_operator
if asset_bar and hasattr(asset_bar, "history_back_button"):
asset_bar.history_back_button.visible = active_tab["history_index"] > 0
asset_bar.history_forward_button.visible = False
asset_bar.update_tab_icons()
return history_step
def get_history_step(history_step_id):
return global_vars.DATA["history steps"].get(history_step_id)