2025-12-01
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user