work
save startup blend for animation tab & whatnot
This commit is contained in:
@@ -20,7 +20,6 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
@@ -35,12 +34,14 @@ from bpy.types import Operator
|
||||
|
||||
from . import (
|
||||
asset_bar_op,
|
||||
categories,
|
||||
client_lib,
|
||||
client_tasks,
|
||||
comments_utils,
|
||||
datas,
|
||||
download,
|
||||
global_vars,
|
||||
icons,
|
||||
image_utils,
|
||||
paths,
|
||||
reports,
|
||||
@@ -50,11 +51,340 @@ from . import (
|
||||
utils,
|
||||
)
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
else:
|
||||
override_extension_draw = None
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_PAGE_SIZE = 80
|
||||
"""Maximum number of assets to fetch per search page."""
|
||||
|
||||
search_tasks = {}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Active filter helpers (per tab)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ensure_tab_filters(tab: dict) -> list[dict]:
|
||||
if "active_filters" not in tab:
|
||||
tab["active_filters"] = []
|
||||
if tab["active_filters"] is None:
|
||||
tab["active_filters"] = []
|
||||
return tab["active_filters"]
|
||||
|
||||
|
||||
def get_active_filters(tab: Optional[dict] = None) -> list[dict]:
|
||||
tab = tab or get_active_tab()
|
||||
return copy.deepcopy(_ensure_tab_filters(tab))
|
||||
|
||||
|
||||
PANEL_FILTER_TERMS: set[str] = {
|
||||
"style",
|
||||
"geometry_nodes",
|
||||
"design_year",
|
||||
"polycount",
|
||||
"texture_resolution",
|
||||
"file_size",
|
||||
"condition",
|
||||
"animated",
|
||||
"free_only",
|
||||
"bookmarks",
|
||||
"quality_limit",
|
||||
"license",
|
||||
"blender_version",
|
||||
"order",
|
||||
"category",
|
||||
}
|
||||
|
||||
|
||||
def _collect_panel_filters() -> list[dict]:
|
||||
"""Translate UI filter state into active filter chip descriptors."""
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
sprops = utils.get_search_props()
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
|
||||
panel_filters: list[dict] = []
|
||||
|
||||
if ui_props.free_only:
|
||||
panel_filters.append(
|
||||
{"term": "free_only", "value": "true", "label": "Free first"}
|
||||
)
|
||||
|
||||
if ui_props.search_bookmarks:
|
||||
panel_filters.append(
|
||||
{"term": "bookmarks", "value": "true", "label": "Bookmarks"}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_style", "ANY") != "ANY":
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "style",
|
||||
"value": sprops.search_style,
|
||||
"label": sprops.search_style.capitalize(),
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_condition", "UNSPECIFIED") != "UNSPECIFIED":
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "condition",
|
||||
"value": sprops.search_condition,
|
||||
"label": sprops.search_condition.capitalize(),
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_design_year", False):
|
||||
label = f"{sprops.search_design_year_min}-{sprops.search_design_year_max}"
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "design_year",
|
||||
"value": label,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_polycount", False):
|
||||
label = f"Poly {sprops.search_polycount_min}-{sprops.search_polycount_max}"
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "polycount",
|
||||
"value": label,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_texture_resolution", False):
|
||||
label = f"TexRes {sprops.search_texture_resolution_min}-{sprops.search_texture_resolution_max}"
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "texture_resolution",
|
||||
"value": label,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_file_size", False):
|
||||
label = f"File {sprops.search_file_size_min}-{sprops.search_file_size_max}MB"
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "file_size",
|
||||
"value": label,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(sprops, "search_animated", False):
|
||||
panel_filters.append({"term": "animated", "value": "true", "label": "Animated"})
|
||||
|
||||
if getattr(sprops, "search_geometry_nodes", False):
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "geometry_nodes",
|
||||
"value": "true",
|
||||
"label": "GeoNodes",
|
||||
}
|
||||
)
|
||||
|
||||
if ui_props.quality_limit > 0:
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "quality_limit",
|
||||
"value": str(ui_props.quality_limit),
|
||||
"label": f"Q≥{ui_props.quality_limit}",
|
||||
}
|
||||
)
|
||||
|
||||
if ui_props.search_license != "ANY":
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "license",
|
||||
"value": ui_props.search_license,
|
||||
"label": ui_props.search_license,
|
||||
}
|
||||
)
|
||||
|
||||
if ui_props.search_blender_version:
|
||||
label = f"Blender {ui_props.search_blender_version_min}-{ui_props.search_blender_version_max}"
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "blender_version",
|
||||
"value": label,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
|
||||
if ui_props.search_order_by != "default":
|
||||
panel_filters.append(
|
||||
{
|
||||
"term": "order",
|
||||
"value": ui_props.search_order_by,
|
||||
"label": ui_props.search_order_by,
|
||||
}
|
||||
)
|
||||
|
||||
# NSFW is intentionally left out; it already changes server query and badge state.
|
||||
|
||||
category = getattr(sprops, "search_category", "")
|
||||
if category and category != ui_props.asset_type.lower():
|
||||
bkit_categories = global_vars.DATA.get("bkit_categories") or []
|
||||
name_path = categories.get_category_name_path(bkit_categories, category)
|
||||
label = name_path[-1] if name_path else category.split("/")[-1]
|
||||
panel_filters.append({"term": "category", "value": category, "label": label})
|
||||
|
||||
return panel_filters
|
||||
|
||||
|
||||
def _sync_panel_filters_into_active(tab: dict):
|
||||
"""Merge panel-derived filters with existing ad-hoc filters (e.g., manufacturer)."""
|
||||
current = _ensure_tab_filters(tab)
|
||||
preserved = [f for f in current if f.get("term") not in PANEL_FILTER_TERMS]
|
||||
tab["active_filters"] = preserved + _collect_panel_filters()
|
||||
|
||||
|
||||
def set_active_filter(
|
||||
term: str,
|
||||
value: str,
|
||||
label: Optional[str] = None,
|
||||
origin: Optional[str] = None,
|
||||
):
|
||||
tab = get_active_tab()
|
||||
filters = _ensure_tab_filters(tab)
|
||||
# drop existing entry for the same term to keep one value per term for now
|
||||
filters = [f for f in filters if f.get("term") != term]
|
||||
filters.append(
|
||||
{"term": term, "value": value, "label": label or value, "origin": origin}
|
||||
)
|
||||
tab["active_filters"] = filters
|
||||
|
||||
|
||||
def remove_active_filter(term: str, value: Optional[str] = None):
|
||||
tab = get_active_tab()
|
||||
filters = _ensure_tab_filters(tab)
|
||||
if term in PANEL_FILTER_TERMS:
|
||||
_clear_panel_filter(term)
|
||||
if value is None:
|
||||
filters = [f for f in filters if f.get("term") != term]
|
||||
else:
|
||||
filters = [
|
||||
f
|
||||
for f in filters
|
||||
if not (f.get("term") == term and f.get("value") == value)
|
||||
]
|
||||
tab["active_filters"] = filters
|
||||
|
||||
|
||||
def set_active_filters_for_tab(tab: dict, filters: list[dict]):
|
||||
tab["active_filters"] = copy.deepcopy(filters) if filters else []
|
||||
|
||||
|
||||
def search_by_author_id(author_id: str, author_name: str = ""):
|
||||
"""Set author filter, clean keywords of author name parts, and run search.
|
||||
|
||||
This is the single entry point for all "search by author" actions:
|
||||
asset bar click, keyboard shortcut, popup card button, etc.
|
||||
"""
|
||||
author_id = str(author_id)
|
||||
if not author_name or author_name == author_id:
|
||||
author = global_vars.BKIT_AUTHORS.get(int(author_id))
|
||||
if author:
|
||||
full = f"{author.firstName} {author.lastName}".strip()
|
||||
if full:
|
||||
author_name = full
|
||||
if not author_name:
|
||||
author_name = author_id
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
keywords = ui_props.search_keywords
|
||||
if keywords and author_name:
|
||||
kw_parts = keywords.split()
|
||||
if len(kw_parts) <= 1:
|
||||
# Single word — user was clearly searching for this author, clear it
|
||||
keywords = ""
|
||||
else:
|
||||
name_parts = author_name.lower().split()
|
||||
keywords = " ".join(w for w in kw_parts if w.lower() not in name_parts)
|
||||
# Strip any legacy +author_id: from keywords
|
||||
keywords = re.sub(r"\+author_id:\d+", "", keywords).strip()
|
||||
ui_props.search_keywords = keywords
|
||||
|
||||
sprops = utils.get_search_props()
|
||||
if utils.profile_is_validator():
|
||||
sprops.search_verification_status = "ALL"
|
||||
|
||||
set_active_filter(
|
||||
term="author_id",
|
||||
value=author_id,
|
||||
label=author_name,
|
||||
origin="data",
|
||||
)
|
||||
update_filters()
|
||||
create_history_step(get_active_tab())
|
||||
search()
|
||||
|
||||
|
||||
def _clear_panel_filter(term: str):
|
||||
"""Reset underlying filter props when a panel-derived chip is removed."""
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
sprops = utils.get_search_props()
|
||||
|
||||
if term == "style" and hasattr(sprops, "search_style"):
|
||||
sprops.search_style = "ANY"
|
||||
elif term == "condition" and hasattr(sprops, "search_condition"):
|
||||
sprops.search_condition = "UNSPECIFIED"
|
||||
elif term == "design_year" and hasattr(sprops, "search_design_year"):
|
||||
sprops.search_design_year = False
|
||||
elif term == "polycount" and hasattr(sprops, "search_polycount"):
|
||||
sprops.search_polycount = False
|
||||
elif term == "texture_resolution" and hasattr(sprops, "search_texture_resolution"):
|
||||
sprops.search_texture_resolution = False
|
||||
elif term == "file_size" and hasattr(sprops, "search_file_size"):
|
||||
sprops.search_file_size = False
|
||||
elif term == "animated" and hasattr(sprops, "search_animated"):
|
||||
sprops.search_animated = False
|
||||
elif term == "geometry_nodes" and hasattr(sprops, "search_geometry_nodes"):
|
||||
sprops.search_geometry_nodes = False
|
||||
elif term == "free_only":
|
||||
ui_props.free_only = False
|
||||
elif term == "bookmarks":
|
||||
ui_props.search_bookmarks = False
|
||||
elif term == "quality_limit":
|
||||
ui_props.quality_limit = 0
|
||||
elif term == "license":
|
||||
ui_props.search_license = "ANY"
|
||||
elif term == "blender_version":
|
||||
ui_props.search_blender_version = False
|
||||
elif term == "order":
|
||||
ui_props.search_order_by = "default"
|
||||
elif term == "category" and hasattr(sprops, "search_category"):
|
||||
sprops.search_category = ""
|
||||
# Reset the category browse path back to the root for this asset type
|
||||
asset_type = ui_props.asset_type
|
||||
active_browse = global_vars.DATA.get("active_category_browse")
|
||||
if active_browse is not None and asset_type in active_browse:
|
||||
active_browse[asset_type] = [asset_type.lower()]
|
||||
|
||||
|
||||
def get_active_filter_keywords(tab: Optional[dict] = None) -> list[str]:
|
||||
tab = tab or get_active_tab()
|
||||
filters = _ensure_tab_filters(tab)
|
||||
tokens = []
|
||||
for f in filters:
|
||||
term = f.get("term")
|
||||
value = f.get("value")
|
||||
# Panel-derived filters (style, free_only, order, etc.) are represented
|
||||
# directly in query parameters and should not emit keyword tokens.
|
||||
if term in PANEL_FILTER_TERMS:
|
||||
continue
|
||||
if term and value:
|
||||
tokens.append(f"+{term}:{value}")
|
||||
return tokens
|
||||
|
||||
|
||||
def _inject_user_price_data(assets: list[dict]) -> None:
|
||||
"""Augment search results with per-user pricing info when available."""
|
||||
if not assets:
|
||||
@@ -67,6 +397,7 @@ def _inject_user_price_data(assets: list[dict]) -> None:
|
||||
return
|
||||
|
||||
try:
|
||||
# returns entry per versionUuid
|
||||
price_response = search_price.query_user_price(
|
||||
version_uuids=version_uuids,
|
||||
page_size=len(version_uuids),
|
||||
@@ -84,17 +415,17 @@ def _inject_user_price_data(assets: list[dict]) -> None:
|
||||
|
||||
price_by_uuid: dict[str, dict] = {}
|
||||
for entry in price_response:
|
||||
version_uuid = entry.get("versionUuid") # maybe assetUuid ?
|
||||
if not version_uuid:
|
||||
base_uuid = entry.get("versionUuid")
|
||||
if not base_uuid:
|
||||
continue
|
||||
price_by_uuid[version_uuid] = entry
|
||||
price_by_uuid[base_uuid] = entry
|
||||
|
||||
if not price_by_uuid:
|
||||
return
|
||||
|
||||
for asset in assets:
|
||||
version_uuid = asset["id"]
|
||||
price_info = price_by_uuid.get(version_uuid)
|
||||
base_uuid = asset["id"]
|
||||
price_info = price_by_uuid.get(base_uuid)
|
||||
if not price_info:
|
||||
continue
|
||||
asset["userPrice"] = price_info["discountedPrice"]
|
||||
@@ -185,7 +516,7 @@ def check_clipboard():
|
||||
try: # could be problematic on Linux
|
||||
current_clipboard = str(bpy.context.window_manager.clipboard)
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Failed to get clipboard: {e}")
|
||||
bk_logger.warning("Failed to get clipboard: %s", e)
|
||||
return
|
||||
|
||||
if current_clipboard == last_clipboard:
|
||||
@@ -217,6 +548,8 @@ def check_clipboard():
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("addon") > -1 or asset_type_string.find("add-on") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
elif asset_type_string.find("artist") > -1 or asset_type_string.find("author") > -1:
|
||||
target_asset_type = "AUTHOR"
|
||||
else:
|
||||
bk_logger.debug("Clipboard does not contain valid asset type.")
|
||||
return
|
||||
@@ -229,6 +562,73 @@ def check_clipboard():
|
||||
ui_props.search_keywords = current_clipboard[:asset_type_index].rstrip()
|
||||
|
||||
|
||||
def parse_author_result(r) -> dict:
|
||||
"""Parse an author-type search result into asset_data with safe defaults.
|
||||
|
||||
Author results have full author data in the ``author`` sub-object (same
|
||||
structure as regular assets). We use it to populate ``BKIT_AUTHORS`` and
|
||||
fetch the gravatar, then synthesize the remaining fields that downstream
|
||||
code expects but the server doesn't provide for authors.
|
||||
"""
|
||||
author_id = r.get("id", r.get("author", {}).get("id", 0))
|
||||
display_name = r.get("displayName", r.get("name", ""))
|
||||
|
||||
asset_data = {
|
||||
"thumbnail": "",
|
||||
"thumbnail_small": "",
|
||||
"downloaded": 0,
|
||||
"available_resolutions": [],
|
||||
"max_resolution": 0,
|
||||
"filesSize": 0,
|
||||
"dictParameters": {},
|
||||
"files": [],
|
||||
"verificationStatus": "validated",
|
||||
"canDownload": False,
|
||||
"isFree": True,
|
||||
"score": 0,
|
||||
"ratingsCount": {},
|
||||
"ratingsAverage": {},
|
||||
"assetBaseId": str(author_id),
|
||||
"displayName": display_name,
|
||||
}
|
||||
|
||||
# Process full author profile data (fetches gravatar, populates BKIT_AUTHORS)
|
||||
# NOTE: generate_author_profile sends id to the GO client which expects int,
|
||||
# so we must NOT convert id to string before calling it (same as parse_result).
|
||||
adata = r.get("author")
|
||||
if adata and isinstance(adata, dict) and len(adata) > 1:
|
||||
# Full author data available — parse it like regular assets do
|
||||
adata = dict(adata) # copy so pop() doesn't mutate the original
|
||||
social_networks = datas.parse_social_networks(
|
||||
adata.pop("socialNetworks", None) or []
|
||||
)
|
||||
author = datas.UserProfile(**adata, socialNetworks=social_networks)
|
||||
generate_author_profile(author)
|
||||
r["author"]["id"] = str(r["author"]["id"])
|
||||
else:
|
||||
# Minimal author data — just ensure id is a string
|
||||
if "author" not in r:
|
||||
r["author"] = {"id": str(author_id)}
|
||||
else:
|
||||
r["author"]["id"] = str(r["author"]["id"])
|
||||
|
||||
# Apply server data, then re-apply safe defaults for any fields that ended
|
||||
# up as None or empty (Go serializes nil maps/slices as null → Python None,
|
||||
# and some string fields come back as "" instead of a valid value).
|
||||
safe_defaults = {
|
||||
"dictParameters": {},
|
||||
"files": [],
|
||||
"ratingsCount": {},
|
||||
"ratingsAverage": {},
|
||||
"verificationStatus": "validated",
|
||||
}
|
||||
asset_data.update(r)
|
||||
for key, default in safe_defaults.items():
|
||||
if not asset_data.get(key):
|
||||
asset_data[key] = default
|
||||
return asset_data
|
||||
|
||||
|
||||
# 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:
|
||||
@@ -248,6 +648,9 @@ def parse_result(r) -> dict:
|
||||
utils.p("asset with no files-size")
|
||||
|
||||
asset_type = r["assetType"]
|
||||
if asset_type == "author":
|
||||
return parse_author_result(r)
|
||||
|
||||
adata = r["author"]
|
||||
social_networks = datas.parse_social_networks(adata.pop("socialNetworks", []))
|
||||
author = datas.UserProfile(**adata, socialNetworks=social_networks)
|
||||
@@ -302,7 +705,7 @@ def parse_result(r) -> dict:
|
||||
r["max_resolution"] = max(r["available_resolutions"])
|
||||
|
||||
# tooltip = generate_tooltip(r)
|
||||
# for some reason, the id was still int on some occurances. investigate this.
|
||||
# for some reason, the id was still int on some occurrences. investigate this.
|
||||
r["author"]["id"] = str(r["author"]["id"])
|
||||
|
||||
# some helper props, but generally shouldn't be renaming/duplifiying original properties,
|
||||
@@ -373,7 +776,7 @@ def cleanup_search_results():
|
||||
|
||||
|
||||
def handle_search_task_error(task: client_tasks.Task) -> None:
|
||||
"""Handle incomming search task error."""
|
||||
"""Handle incoming search task error."""
|
||||
# First find the history step that the task belongs to
|
||||
for history_step in get_history_steps().values():
|
||||
if task.task_id in history_step.get("search_tasks", {}).keys():
|
||||
@@ -434,6 +837,8 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
result_field.append(asset_data)
|
||||
if not utils.profile_is_validator():
|
||||
continue
|
||||
if asset_data.get("assetType") == "author":
|
||||
continue
|
||||
# VALIDATORS
|
||||
# fetch all comments if user is validator to preview them faster
|
||||
# these comments are also shown as part of the tooltip oh mouse hover in asset bar.
|
||||
@@ -441,6 +846,12 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
if comments is None:
|
||||
client_lib.get_comments(asset_data["assetBaseId"])
|
||||
|
||||
# Separate author results from regular assets, put authors first
|
||||
author_results = [r for r in result_field if r.get("assetType") == "author"]
|
||||
asset_results = [r for r in result_field if r.get("assetType") != "author"]
|
||||
|
||||
result_field = author_results + asset_results
|
||||
|
||||
# Apply addon-specific status checking and filtering if needed
|
||||
if ui_props.asset_type == "ADDON":
|
||||
# Always process addon search results to store installation status
|
||||
@@ -450,14 +861,18 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
|
||||
addon_props = bpy.context.window_manager.blenderkit_addon
|
||||
if addon_props.search_installed:
|
||||
# Filter to only show installed addons
|
||||
# Filter to only show installed addons, but preserve author results
|
||||
result_field = [
|
||||
asset for asset in result_field if asset.get("downloaded", 0) > 0
|
||||
asset
|
||||
for asset in result_field
|
||||
if asset.get("assetType") == "author" or asset.get("downloaded", 0) > 0
|
||||
]
|
||||
|
||||
# TODO: if ever needed, implement for other future types
|
||||
if result_field:
|
||||
_inject_user_price_data(result_field)
|
||||
if override_extension_draw is not None:
|
||||
override_extension_draw.update_cache_with_asset_prices(result_field)
|
||||
|
||||
# Store results in history step
|
||||
history_step["search_results"] = result_field
|
||||
@@ -512,6 +927,11 @@ def handle_thumbnail_download_task(task: client_tasks.Task) -> None:
|
||||
|
||||
if task.data["thumbnail_type"] == "full":
|
||||
asset_bar_op.asset_bar_operator.update_tooltip_image(task.data["assetBaseId"])
|
||||
return
|
||||
|
||||
if task.data["thumbnail_type"] in {"photo_full", "wire_full"}:
|
||||
asset_bar_op.asset_bar_operator.needs_tooltip_update = True
|
||||
return
|
||||
|
||||
|
||||
def load_preview(asset):
|
||||
@@ -623,6 +1043,7 @@ def write_block_from_value(tooltip, value, pretext="", width=2000): # for longe
|
||||
if not value:
|
||||
return tooltip
|
||||
|
||||
intext = value
|
||||
if type(value) == list:
|
||||
intext = list_to_str(value)
|
||||
elif type(value) == float:
|
||||
@@ -682,11 +1103,14 @@ def generate_author_textblock(first_name: str, last_name: str, about_me: str):
|
||||
|
||||
|
||||
def handle_fetch_gravatar_task(task: client_tasks.Task):
|
||||
"""Handle incomming fetch_gravatar_task which contains path to author's image on the disk."""
|
||||
"""Handle incoming fetch_gravatar_task which contains path to author's image on the disk."""
|
||||
if task.status == "finished":
|
||||
author_id = int(task.data["id"])
|
||||
gravatar_path = task.result["gravatar_path"]
|
||||
global_vars.BKIT_AUTHORS[author_id].gravatarImg = gravatar_path
|
||||
# Notify asset bar to refresh author thumbnails
|
||||
if asset_bar_op.asset_bar_operator is not None:
|
||||
asset_bar_op.asset_bar_operator.update_image(str(author_id))
|
||||
|
||||
|
||||
def generate_author_profile(author_data: datas.UserProfile):
|
||||
@@ -708,12 +1132,12 @@ def generate_author_profile(author_data: datas.UserProfile):
|
||||
|
||||
|
||||
def handle_get_user_profile(task: client_tasks.Task):
|
||||
"""Handle incomming get_user_profile task which contains data about current logged-in user."""
|
||||
"""Handle incoming get_user_profile task which contains data about current logged-in user."""
|
||||
if task.status not in ["finished", "error"]:
|
||||
return
|
||||
|
||||
if task.status == "error":
|
||||
bk_logger.warning(f"Could not load user profile: {task.message}")
|
||||
bk_logger.warning("Could not load user profile: %s", task.message)
|
||||
return
|
||||
|
||||
user_data = task.result.get("user")
|
||||
@@ -782,7 +1206,13 @@ def query_to_url(
|
||||
for q in query:
|
||||
if q in ["query", "free_first", "search_order_by"]:
|
||||
continue
|
||||
requeststring += f"+{q}:{urllib.parse.quote_plus(str(query[q]))}"
|
||||
value = str(query[q])
|
||||
if q == "asset_type" and value != "author":
|
||||
has_keywords = query.get("query") not in ("", None)
|
||||
has_author_filter = query.get("author_id") not in ("", None)
|
||||
if has_keywords and not has_author_filter:
|
||||
value += ",author"
|
||||
requeststring += f"+{q}:{urllib.parse.quote_plus(value)}"
|
||||
|
||||
# add dict_parameters to make results smaller
|
||||
|
||||
@@ -864,9 +1294,15 @@ def build_query_common(query: dict, props, ui_props) -> dict:
|
||||
"""
|
||||
query = copy.deepcopy(query)
|
||||
query_common = {}
|
||||
if ui_props.search_keywords != "":
|
||||
keywords = ui_props.search_keywords.replace("&", "%26")
|
||||
query_common["query"] = keywords
|
||||
base_keywords = ui_props.search_keywords.strip()
|
||||
filter_tokens = get_active_filter_keywords()
|
||||
combined_parts = []
|
||||
if base_keywords:
|
||||
combined_parts.append(base_keywords.replace("&", "%26"))
|
||||
combined_parts.extend(filter_tokens)
|
||||
combined_keywords = " ".join(part for part in combined_parts if part)
|
||||
if combined_keywords:
|
||||
query_common["query"] = combined_keywords
|
||||
|
||||
if props.search_verification_status != "ALL" and utils.profile_is_validator():
|
||||
query_common["verification_status"] = props.search_verification_status.lower()
|
||||
@@ -1009,6 +1445,26 @@ def build_query_addon(props, ui_props) -> dict:
|
||||
return build_query_common(query, props, ui_props)
|
||||
|
||||
|
||||
def build_query_author(props, ui_props) -> dict:
|
||||
"""Pure function to construct search query dict for authors."""
|
||||
query = {"asset_type": "author"}
|
||||
query = build_query_common(query, props, ui_props)
|
||||
# +author_id:XXX doesn't match author profile documents in elasticsearch
|
||||
# (that field only exists on asset documents). Replace it with the
|
||||
# author's name so the API can do a text-search for the profile instead.
|
||||
q = query.get("query", "")
|
||||
match = re.search(r"\+author_id:(\d+)", q)
|
||||
if match:
|
||||
aid = int(match.group(1))
|
||||
author = global_vars.BKIT_AUTHORS.get(aid)
|
||||
name = author.fullName if author else ""
|
||||
q = re.sub(r"\+author_id:\d+\s*", "", q).strip()
|
||||
if name:
|
||||
q = f"{name} {q}".strip() if q else name
|
||||
query["query"] = q or None
|
||||
return query
|
||||
|
||||
|
||||
def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
"""
|
||||
Filter addon search results based on local installation status.
|
||||
@@ -1026,10 +1482,8 @@ def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
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)
|
||||
if not filter_installed_only:
|
||||
filtered_results.append(asset)
|
||||
continue
|
||||
|
||||
# Check installation and enablement status for addon
|
||||
@@ -1054,7 +1508,9 @@ def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
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}"
|
||||
"Could not determine installation status for addon %s : %s",
|
||||
asset.get("name", "Unknown"),
|
||||
e,
|
||||
)
|
||||
asset["downloaded"] = 0
|
||||
asset["enabled"] = False
|
||||
@@ -1135,7 +1591,7 @@ def get_search_simple(
|
||||
page_index = 2
|
||||
page_count = math.ceil(search_results["count"] / page_size)
|
||||
while search_results.get("next") and len(results) < max_results:
|
||||
bk_logger.info(f"getting page {page_index} , total pages {page_count}")
|
||||
bk_logger.info("getting page %d , total pages %d", page_index, page_count)
|
||||
response = client_lib.blocking_request(search_results["next"], "GET", headers)
|
||||
search_results = response.json()
|
||||
results.extend(search_results["results"])
|
||||
@@ -1146,7 +1602,7 @@ def get_search_simple(
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as s:
|
||||
json.dump(results, s, ensure_ascii=False, indent=4)
|
||||
bk_logger.info(f"retrieved {len(results)} assets from elastic search")
|
||||
bk_logger.info("retrieved %d assets from elastic search", len(results))
|
||||
return results
|
||||
|
||||
|
||||
@@ -1249,6 +1705,12 @@ def search(get_next=False, query=None, author_id=""):
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
if ui_props.asset_type == "AUTHOR":
|
||||
query = build_query_author(
|
||||
props=bpy.context.window_manager.blenderkit_author,
|
||||
ui_props=bpy.context.window_manager.blenderkitUI,
|
||||
)
|
||||
|
||||
# crop long searches
|
||||
if query.get("query"):
|
||||
if len(query["query"]) > 50:
|
||||
@@ -1278,8 +1740,9 @@ def search(get_next=False, query=None, author_id=""):
|
||||
|
||||
active_history_step["is_searching"] = True
|
||||
|
||||
page_size = min(40, ui_props.wcount * user_preferences.maximized_assetbar_rows + 5)
|
||||
|
||||
page_size = min(
|
||||
MAX_PAGE_SIZE, ui_props.wcount * user_preferences.maximized_assetbar_rows + 5
|
||||
)
|
||||
next_url = ""
|
||||
if get_next and active_history_step.get("search_results_orig"):
|
||||
next_url = active_history_step["search_results_orig"].get("next", "")
|
||||
@@ -1292,6 +1755,7 @@ def clean_filters():
|
||||
"""Cleanup filters in case search needs to be reset, typically when asset id is copy pasted."""
|
||||
sprops = utils.get_search_props()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
active_tab = get_active_tab()
|
||||
ui_props.property_unset("own_only")
|
||||
sprops.property_unset("search_texture_resolution")
|
||||
sprops.property_unset("search_file_size")
|
||||
@@ -1326,6 +1790,7 @@ def update_filters():
|
||||
|
||||
sprops = utils.get_search_props()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
active_tab = get_active_tab()
|
||||
|
||||
if ui_props.search_bookmarks and not utils.user_logged_in():
|
||||
ui_props.search_bookmarks = False
|
||||
@@ -1341,6 +1806,8 @@ def update_filters():
|
||||
)
|
||||
return False
|
||||
|
||||
_sync_panel_filters_into_active(active_tab)
|
||||
|
||||
fcommon = (
|
||||
ui_props.own_only
|
||||
or sprops.search_texture_resolution
|
||||
@@ -1352,6 +1819,7 @@ def update_filters():
|
||||
or ui_props.search_license != "ANY"
|
||||
or ui_props.search_blender_version
|
||||
or ui_props.search_order_by != "default"
|
||||
or len(get_active_filters()) > 0
|
||||
# NSFW filter is signaled in a special way and should not affect the filter icon
|
||||
)
|
||||
|
||||
@@ -1377,6 +1845,8 @@ def update_filters():
|
||||
sprops.use_filters = fcommon
|
||||
elif ui_props.asset_type == "ADDON":
|
||||
sprops.use_filters = fcommon
|
||||
elif ui_props.asset_type == "AUTHOR":
|
||||
sprops.use_filters = fcommon
|
||||
return True
|
||||
|
||||
|
||||
@@ -1428,6 +1898,8 @@ def detect_asset_type_from_keywords(keywords: str) -> tuple[str, str]:
|
||||
"addon": "ADDON",
|
||||
"add-on": "ADDON",
|
||||
"extension": "ADDON",
|
||||
"artist": "AUTHOR",
|
||||
"author": "AUTHOR",
|
||||
}
|
||||
|
||||
# Convert to lowercase for matching
|
||||
@@ -1449,6 +1921,20 @@ def search_update(self, context):
|
||||
# when search is locked, don't trigger search update
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
|
||||
# Drop data-driven filters (e.g. manufacturer chips) when switching asset type.
|
||||
# Seed the tracker on first run to avoid wiping filters during the initial update.
|
||||
last_asset_type = getattr(ui_props, "_last_asset_type", None)
|
||||
if last_asset_type is None:
|
||||
ui_props._last_asset_type = ui_props.asset_type
|
||||
elif last_asset_type != ui_props.asset_type:
|
||||
tab = get_active_tab()
|
||||
filters = _ensure_tab_filters(tab)
|
||||
# Keep only panel-defined filters; drop all ad-hoc/data-derived ones
|
||||
tab["active_filters"] = [
|
||||
f for f in filters if f.get("term") in PANEL_FILTER_TERMS
|
||||
]
|
||||
ui_props._last_asset_type = ui_props.asset_type
|
||||
|
||||
if ui_props.search_lock:
|
||||
return
|
||||
|
||||
@@ -1507,6 +1993,11 @@ def search_update(self, context):
|
||||
target_asset_type = "PRINTABLE"
|
||||
elif asset_type_string.find("addon") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
elif (
|
||||
asset_type_string.find("artist") > -1
|
||||
or asset_type_string.find("author") > -1
|
||||
):
|
||||
target_asset_type = "AUTHOR"
|
||||
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.search_keywords = ""
|
||||
@@ -1541,9 +2032,9 @@ def strip_accents(s):
|
||||
|
||||
def refresh_search():
|
||||
"""Refresh search results. Useful after login/logout."""
|
||||
props = utils.get_search_props()
|
||||
if props is not None:
|
||||
props.report = ""
|
||||
sprops = utils.get_search_props()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
active_tab = get_active_tab()
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.assetbar_on:
|
||||
@@ -1631,20 +2122,18 @@ class SearchOperator(Operator):
|
||||
|
||||
search_keywords = str(ui_props.search_keywords)
|
||||
|
||||
if self.keywords != "":
|
||||
search_keywords = self.keywords
|
||||
|
||||
# remove all search keywords if force_clear is set
|
||||
if self.force_clear:
|
||||
# self.force_clear = False # reset the force clear
|
||||
search_keywords = ""
|
||||
|
||||
if self.keywords != "":
|
||||
search_keywords = self.keywords
|
||||
|
||||
if self.author_id != "":
|
||||
bk_logger.info(f"Author ID: {self.author_id}")
|
||||
# if there is already an author id in the search keywords, remove it first, the author_id can be any so
|
||||
# use regex to find it
|
||||
search_keywords = re.sub(r"\+author_id:\d+", "", search_keywords)
|
||||
search_keywords += f"+author_id:{self.author_id}"
|
||||
ui_props.search_keywords = search_keywords
|
||||
search_by_author_id(self.author_id)
|
||||
return {"FINISHED"}
|
||||
|
||||
ui_props.search_keywords = search_keywords
|
||||
|
||||
@@ -1704,7 +2193,115 @@ def get_search_similar_keywords(asset_data: dict) -> str:
|
||||
return keywords
|
||||
|
||||
|
||||
classes = [SearchOperator, UrlOperator, TooltipLabelOperator]
|
||||
class AuthorAssetTypeSearch(Operator):
|
||||
"""Switch to a specific asset type tab and search by author"""
|
||||
|
||||
bl_idname = "view3d.blenderkit_author_asset_type_search"
|
||||
bl_label = "Search Author Assets"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
author_id: StringProperty(name="Author ID", default="", options={"SKIP_SAVE"})
|
||||
author_name: StringProperty(name="Author Name", default="", options={"SKIP_SAVE"})
|
||||
asset_type: StringProperty(
|
||||
name="Asset Type", default="MODEL", options={"SKIP_SAVE"}
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_props.search_lock = True
|
||||
ui_props.asset_type = self.asset_type
|
||||
ui_props.search_keywords = ""
|
||||
ui_props.search_lock = False
|
||||
search_by_author_id(self.author_id, self.author_name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AuthorAssetTypePopup(Operator):
|
||||
"""Choose which asset type to browse for this author"""
|
||||
|
||||
bl_idname = "view3d.blenderkit_author_asset_type_popup"
|
||||
bl_label = "Find Author's Assets"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
author_id: StringProperty(name="Author ID", default="", options={"SKIP_SAVE"})
|
||||
author_name: StringProperty(name="Author Name", default="", options={"SKIP_SAVE"})
|
||||
|
||||
# Set by caller before invoke — per-type asset counts from the author result
|
||||
_asset_type_counts: dict = {}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_popup(self, width=200)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text=self.author_name or "Author")
|
||||
layout.separator()
|
||||
|
||||
counts = self._asset_type_counts
|
||||
pcoll = icons.icon_collections["main"]
|
||||
asset_types = [
|
||||
("MODEL", "Models", "OBJECT_DATAMODE", "model"),
|
||||
("MATERIAL", "Materials", "MATERIAL", "material"),
|
||||
("SCENE", "Scenes", "SCENE_DATA", "scene"),
|
||||
("HDR", "HDRs", "WORLD", "hdr"),
|
||||
("BRUSH", "Brushes", "BRUSH_DATA", "brush"),
|
||||
("NODEGROUP", "Node Groups", "NODETREE", "nodegroup"),
|
||||
]
|
||||
|
||||
for at_id, at_label, at_icon, at_key in asset_types:
|
||||
count = counts.get(at_key, 0)
|
||||
if counts and count == 0:
|
||||
continue
|
||||
label = f"{at_label} ({count})" if count else at_label
|
||||
op = layout.operator(
|
||||
"view3d.blenderkit_author_asset_type_search",
|
||||
text=label,
|
||||
icon=at_icon,
|
||||
)
|
||||
op.author_id = self.author_id
|
||||
op.author_name = self.author_name
|
||||
op.asset_type = at_id
|
||||
|
||||
# Printable
|
||||
printable_count = counts.get("printable", 0)
|
||||
if not counts or printable_count > 0:
|
||||
label = (
|
||||
f"Printables ({printable_count})" if printable_count else "Printables"
|
||||
)
|
||||
op = layout.operator(
|
||||
"view3d.blenderkit_author_asset_type_search",
|
||||
text=label,
|
||||
icon_value=pcoll["asset_type_printable"].icon_id,
|
||||
)
|
||||
op.author_id = self.author_id
|
||||
op.author_name = self.author_name
|
||||
op.asset_type = "PRINTABLE"
|
||||
|
||||
# Add-ons (Blender 4.2+)
|
||||
addon_count = counts.get("addon", 0)
|
||||
if bpy.app.version >= (4, 2, 0) and (not counts or addon_count > 0):
|
||||
label = f"Add-ons ({addon_count})" if addon_count else "Add-ons"
|
||||
op = layout.operator(
|
||||
"view3d.blenderkit_author_asset_type_search",
|
||||
text=label,
|
||||
icon="PLUGIN",
|
||||
)
|
||||
op.author_id = self.author_id
|
||||
op.author_name = self.author_name
|
||||
op.asset_type = "ADDON"
|
||||
|
||||
|
||||
classes = [
|
||||
SearchOperator,
|
||||
UrlOperator,
|
||||
TooltipLabelOperator,
|
||||
AuthorAssetTypeSearch,
|
||||
AuthorAssetTypePopup,
|
||||
]
|
||||
|
||||
|
||||
def register_search():
|
||||
@@ -1744,6 +2341,7 @@ def unregister_search():
|
||||
def get_ui_state():
|
||||
"""Get the current UI state."""
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
active_tab = get_active_tab()
|
||||
|
||||
ui_state = {
|
||||
"ui_props": {
|
||||
@@ -1759,6 +2357,7 @@ def get_ui_state():
|
||||
"search_blender_version_max": ui_props.search_blender_version_max,
|
||||
},
|
||||
"search_props": {},
|
||||
"active_filters": get_active_filters(active_tab),
|
||||
}
|
||||
|
||||
# we need to add all props manually since they are a mess now and some should not be stored.
|
||||
@@ -1823,6 +2422,8 @@ def get_ui_state():
|
||||
store_props = store_model_props
|
||||
elif asset_type == "ADDON":
|
||||
store_props = [] # Addons don't need to store specific props
|
||||
elif asset_type == "AUTHOR":
|
||||
store_props = [] # Authors don't need to store specific props
|
||||
|
||||
search_props = utils.get_search_props()
|
||||
|
||||
@@ -1849,15 +2450,13 @@ def update_tab_name(active_tab):
|
||||
|
||||
# Update tab name based on search or category
|
||||
search_keywords = ui_state.get("ui_props", {}).get("search_keywords", "").strip()
|
||||
# if there's author_id let's get the author's name from db of authors
|
||||
# we need to get the number after +author_id:
|
||||
author_id = re.search(r"\+author_id:(\d+)", search_keywords)
|
||||
# Check active filters for author_id
|
||||
author_name = None
|
||||
if author_id is not None:
|
||||
author_id = author_id.group(1)
|
||||
author = global_vars.BKIT_AUTHORS.get(int(author_id))
|
||||
if author:
|
||||
author_name = author.fullName
|
||||
active_filters = ui_state.get("active_filters", [])
|
||||
for flt in active_filters:
|
||||
if flt.get("term") == "author_id":
|
||||
author_name = flt.get("label")
|
||||
break
|
||||
|
||||
search_category = (
|
||||
ui_state.get("search_props", {}).get("search_category", "").strip()
|
||||
@@ -1893,7 +2492,7 @@ def update_tab_name(active_tab):
|
||||
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}")
|
||||
bk_logger.debug("Could not update tab name in UI: %s", e)
|
||||
|
||||
return history_step
|
||||
|
||||
|
||||
Reference in New Issue
Block a user