2025-07-01
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
# mainly update functions and callbacks for ratings properties, here to avoid circular imports.
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import Context, PropertyGroup
|
||||
|
||||
from . import (
|
||||
client_lib,
|
||||
client_tasks,
|
||||
datas,
|
||||
global_vars,
|
||||
icons,
|
||||
reports,
|
||||
tasks_queue,
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_get_rating_task(task: client_tasks.Task):
|
||||
"""Handle incomming get_rating task by saving the results into global_vars."""
|
||||
if task.status == "created":
|
||||
return
|
||||
if task.status == "error":
|
||||
return bk_logger.warning(f"{task.task_type} task failed: {task.message}")
|
||||
|
||||
asset_id = task.data["asset_id"]
|
||||
ratings = task.result["results"]
|
||||
if len(ratings) == 0:
|
||||
store_rating_local(asset_id, "quality", None)
|
||||
store_rating_local(asset_id, "working_hours", None)
|
||||
return
|
||||
|
||||
for rating in ratings:
|
||||
store_rating_local(asset_id, rating["ratingType"], rating["score"])
|
||||
|
||||
|
||||
def handle_get_ratings_task(task: client_tasks.Task):
|
||||
"""Handle incomming get_ratings task. This is a special task used only by validators which fetches the ratings
|
||||
in big batch right after the search results come into the Client. This is used only to signal problems in the
|
||||
Goroutine which fetches the ratings. The individual ratings are then sent as normal 'get_rating' tasks.
|
||||
"""
|
||||
if task.status == "error": # only reason this task type exists right now
|
||||
return bk_logger.warning(f"{task.task_type} task failed: {task.message}")
|
||||
|
||||
|
||||
def handle_get_bookmarks_task(task: client_tasks.Task):
|
||||
"""Handle incomming get_bookmarks task by saving the results into global_vars.
|
||||
This is different from standard ratings - the results come from elastic search API
|
||||
instead of ratings API.
|
||||
"""
|
||||
if task.status == "created":
|
||||
return
|
||||
if task.status == "error":
|
||||
bk_logger.warning(f"Could not load bookmarks: {task.message}")
|
||||
return
|
||||
|
||||
for asset in task.result["results"]:
|
||||
store_rating_local(asset["id"], "bookmarks", 1)
|
||||
bk_logger.info("Bookmarks loaded")
|
||||
|
||||
|
||||
def handle_send_rating_task(task: client_tasks.Task):
|
||||
"""Handle send rating task."""
|
||||
if task.status == "created":
|
||||
return
|
||||
if task.status == "error":
|
||||
return reports.add_report(
|
||||
task.message, type="ERROR", details=task.message_detailed
|
||||
)
|
||||
if task.status == "finished":
|
||||
if utils.profile_is_validator():
|
||||
return reports.add_report(task.message, type="VALIDATOR")
|
||||
|
||||
|
||||
def store_rating_local(
|
||||
asset_id: str, rating_type: str = "quality", value: Optional[int] = None
|
||||
):
|
||||
"""Store the rating locally in the global_vars.
|
||||
- rating_type can be: "quality", "working_hours", "bookmarks"
|
||||
- value set None to create empty rating and prevent add-on from fetching it again next time
|
||||
"""
|
||||
allowed_rating_types = ["quality", "working_hours", "bookmarks"]
|
||||
if rating_type not in allowed_rating_types:
|
||||
raise ValueError(f"rating_type must be one of {allowed_rating_types}")
|
||||
|
||||
rating = global_vars.RATINGS.get(asset_id, datas.AssetRating())
|
||||
rating.working_hours_fetched = True
|
||||
rating.quality_fetched = True
|
||||
setattr(rating, rating_type, value)
|
||||
global_vars.RATINGS[asset_id] = rating
|
||||
|
||||
|
||||
def get_rating_local(asset_id: str) -> Optional[datas.AssetRating]:
|
||||
"""Get the rating locally from global_vars.RATINGS."""
|
||||
return global_vars.RATINGS.get(asset_id)
|
||||
|
||||
|
||||
def ensure_rating(asset_id: str):
|
||||
"""Ensure the rating is available. First check if it is available in local cache. If it is not then download it from the server.
|
||||
If the rating is present, we need to check if rating.quality_fetched and rating.working_hours_fetched are not False
|
||||
because bookmarked assets will have rating created, but for them the quality and wh was not fetched (bookmarked are get from search
|
||||
and these data does not contain quality and working_hours - and even bookmarked but that can be deduced from searching for bookmarked).
|
||||
"""
|
||||
rating = get_rating_local(asset_id)
|
||||
if rating is None:
|
||||
client_lib.get_rating(asset_id)
|
||||
return
|
||||
if not rating.quality_fetched or rating.working_hours_fetched:
|
||||
client_lib.get_rating(asset_id)
|
||||
|
||||
|
||||
def update_ratings_quality(self, context: Context):
|
||||
if not (hasattr(self, "rating_quality")):
|
||||
# first option is for rating of assets that are from scene
|
||||
asset = self.id_data
|
||||
bkit_ratings = asset.bkit_ratings
|
||||
asset_id = asset["asset_data"]["id"]
|
||||
else:
|
||||
# this part is for operator rating:
|
||||
bkit_ratings = self
|
||||
asset_id = self.asset_id
|
||||
|
||||
local_rating = get_rating_local(self.asset_id)
|
||||
if local_rating is None:
|
||||
local_rating = datas.AssetRating(quality=0)
|
||||
if local_rating.quality == self.rating_quality:
|
||||
return store_rating_local(
|
||||
asset_id, rating_type="quality", value=bkit_ratings.rating_quality
|
||||
)
|
||||
|
||||
store_rating_local(
|
||||
asset_id, rating_type="quality", value=bkit_ratings.rating_quality
|
||||
)
|
||||
if self.rating_quality_lock is True:
|
||||
return
|
||||
|
||||
args = (asset_id, "quality", bkit_ratings.rating_quality)
|
||||
tasks_queue.add_task((client_lib.send_rating, args), wait=0.5, only_last=True)
|
||||
|
||||
|
||||
def update_ratings_work_hours(self, context: Context):
|
||||
if not (hasattr(self, "rating_work_hours")):
|
||||
# first option is for rating of assets that are from scene
|
||||
asset = self.id_data
|
||||
bkit_ratings = asset.bkit_ratings
|
||||
asset_id = asset["asset_data"]["id"]
|
||||
else:
|
||||
# this part is for operator rating:
|
||||
bkit_ratings = self
|
||||
asset_id = self.asset_id
|
||||
|
||||
local_rating = get_rating_local(self.asset_id)
|
||||
if local_rating is None: # rating was not available online
|
||||
local_rating = datas.AssetRating(working_hours=0)
|
||||
|
||||
if local_rating.working_hours == self.rating_work_hours:
|
||||
return store_rating_local(
|
||||
asset_id, rating_type="working_hours", value=bkit_ratings.rating_work_hours
|
||||
)
|
||||
|
||||
store_rating_local(
|
||||
asset_id, rating_type="working_hours", value=bkit_ratings.rating_work_hours
|
||||
)
|
||||
if self.rating_work_hours_lock is True:
|
||||
return
|
||||
|
||||
args = (asset_id, "working_hours", bkit_ratings.rating_work_hours)
|
||||
tasks_queue.add_task((client_lib.send_rating, args), wait=0.5, only_last=True)
|
||||
|
||||
|
||||
def update_quality_ui(self, context: Context):
|
||||
"""Converts the _ui the enum into actual quality number."""
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore
|
||||
api_key = user_preferences.api_key # type: ignore
|
||||
# we need to check for matching value not to update twice/call the popup twice.
|
||||
if api_key == "" and self.rating_quality != int(self.rating_quality_ui):
|
||||
bpy.ops.wm.blenderkit_login( # type: ignore
|
||||
"INVOKE_DEFAULT",
|
||||
message="Please login/signup to rate assets. Clicking OK takes you to web login.",
|
||||
)
|
||||
return
|
||||
|
||||
self.rating_quality = int(self.rating_quality_ui)
|
||||
|
||||
|
||||
def update_ratings_work_hours_ui(self, context: Context):
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore
|
||||
api_key = user_preferences.api_key # type: ignore
|
||||
if api_key == "" and self.rating_work_hours != float(self.rating_work_hours_ui):
|
||||
bpy.ops.wm.blenderkit_login( # type: ignore
|
||||
"INVOKE_DEFAULT",
|
||||
message="Please login/signup to rate assets. Clicking OK takes you to web login.",
|
||||
)
|
||||
return
|
||||
self.rating_work_hours = float(self.rating_work_hours_ui)
|
||||
|
||||
|
||||
def stars_enum_callback(self, context):
|
||||
"""regenerates the enum property used to display rating stars, so that there are filled/empty stars correctly."""
|
||||
items = []
|
||||
for a in range(0, 11):
|
||||
if a == 0:
|
||||
icon = "REMOVE"
|
||||
|
||||
elif self.rating_quality < a:
|
||||
icon = "SOLO_OFF"
|
||||
else:
|
||||
icon = "SOLO_ON"
|
||||
# has to have something before the number in the value, otherwise fails on registration.
|
||||
|
||||
items.append((f"{a}", " ", "", icon, a))
|
||||
return items
|
||||
|
||||
|
||||
def wh_enum_callback(self, context):
|
||||
"""Regenerates working hours enum."""
|
||||
if self.asset_type in ("model", "scene", "printable", "nodegroup"):
|
||||
possible_wh_values = [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
15,
|
||||
20,
|
||||
30,
|
||||
50,
|
||||
100,
|
||||
150,
|
||||
200,
|
||||
250,
|
||||
]
|
||||
elif self.asset_type == "hdr":
|
||||
possible_wh_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
else: # for material, brush assets
|
||||
possible_wh_values = [0, 0.2, 0.5, 1, 2, 3, 4, 5]
|
||||
|
||||
work_hours = self.rating_work_hours
|
||||
if work_hours < 1:
|
||||
work_hours = int(work_hours * 10) / 10
|
||||
else:
|
||||
work_hours = int(work_hours)
|
||||
|
||||
if work_hours not in possible_wh_values:
|
||||
closest_index = 0
|
||||
closest_diff = abs(possible_wh_values[0] - work_hours)
|
||||
for i in range(1, len(possible_wh_values)):
|
||||
diff = abs(possible_wh_values[i] - work_hours)
|
||||
if diff < closest_diff:
|
||||
closest_diff = diff
|
||||
closest_index = i
|
||||
possible_wh_values[closest_index] = work_hours
|
||||
|
||||
items = []
|
||||
items.append(("0", " ", "", "REMOVE", 0))
|
||||
pcoll = icons.icon_collections["main"]
|
||||
|
||||
for w in possible_wh_values:
|
||||
if w > 0:
|
||||
if w < 1:
|
||||
icon_name = f"BK{int(w*10)/10}"
|
||||
else:
|
||||
icon_name = f"BK{int(w)}"
|
||||
if icon_name not in pcoll:
|
||||
icon_name = "bar_slider_up"
|
||||
icon = pcoll[icon_name]
|
||||
# index of the item(last value) is multiplied by 10 to get always integer values that aren't zero
|
||||
items.append((f"{w}", " ", "", icon.icon_id, int(w * 10)))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
class RatingProperties(PropertyGroup):
|
||||
message: StringProperty( # type: ignore
|
||||
name="message",
|
||||
description="message",
|
||||
default="Rating asset",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
asset_id: StringProperty( # type: ignore
|
||||
name="Asset Base Id",
|
||||
description="Unique id of the asset (hidden)",
|
||||
default="",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
asset_name: StringProperty( # type: ignore
|
||||
name="Asset Name",
|
||||
description="Name of the asset (hidden)",
|
||||
default="",
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
asset_type: StringProperty( # type: ignore
|
||||
name="Asset type", description="asset type", default="", options={"SKIP_SAVE"}
|
||||
)
|
||||
|
||||
### QUALITY RATING
|
||||
rating_quality_lock: BoolProperty( # type: ignore
|
||||
name="Quality Lock",
|
||||
description="Quality is locked -> rating is not sent online",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
rating_quality: IntProperty( # type: ignore
|
||||
name="Quality",
|
||||
description="quality of the material",
|
||||
default=0,
|
||||
min=-1,
|
||||
max=10,
|
||||
update=update_ratings_quality,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
# the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily.
|
||||
rating_quality_ui: EnumProperty( # type: ignore
|
||||
name="Quality",
|
||||
items=stars_enum_callback,
|
||||
description="Rate the quality of the asset from 1 to 10 stars.\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu",
|
||||
default=0,
|
||||
update=update_quality_ui,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
### WORK HOURS RATING
|
||||
rating_work_hours_lock: BoolProperty( # type: ignore
|
||||
name="Work Hours Lock",
|
||||
description="Work hours are locked -> rating is not sent online",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
rating_work_hours: FloatProperty( # type: ignore
|
||||
name="Work Hours",
|
||||
description="nonUI How many hours did this work take?\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu.",
|
||||
default=0.00,
|
||||
min=0.0,
|
||||
max=300,
|
||||
update=update_ratings_work_hours,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
rating_work_hours_ui: EnumProperty( # type: ignore
|
||||
name="Work Hours",
|
||||
description="UI How many hours did this work take?\nShortcut: Hover over asset in the asset bar and press 'R' to show rating menu",
|
||||
items=wh_enum_callback,
|
||||
default=0,
|
||||
update=update_ratings_work_hours_ui,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def prefill_ratings(self) -> None:
|
||||
"""Pre-fill the quality and work hours ratings if available.
|
||||
Locks the ratings locks so that the update function is not called and ratings are not sent online.
|
||||
"""
|
||||
if not utils.user_logged_in():
|
||||
return
|
||||
rating = get_rating_local(self.asset_id)
|
||||
if rating is None:
|
||||
return
|
||||
if rating.quality is None and rating.working_hours is None:
|
||||
return
|
||||
if self.rating_quality != 0:
|
||||
return # return if the rating was already filled
|
||||
if self.rating_work_hours != 0:
|
||||
return # return if the rating was already filled
|
||||
|
||||
if rating.quality is not None:
|
||||
self.rating_quality_lock = True
|
||||
self.rating_quality = int(rating.quality)
|
||||
self.rating_quality_lock = False
|
||||
|
||||
if rating.working_hours is not None:
|
||||
wh: Union[float, int]
|
||||
if rating.working_hours >= 1:
|
||||
wh = int(rating.working_hours)
|
||||
else:
|
||||
wh = round(rating.working_hours, 1)
|
||||
whs = str(wh)
|
||||
self.rating_work_hours_lock = True
|
||||
self.rating_work_hours = round(rating.working_hours, 2)
|
||||
try:
|
||||
# when the value is not in the enum, it throws an error
|
||||
if whs == "0.0":
|
||||
whs = "0"
|
||||
self.rating_work_hours_ui = whs
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"exception setting rating_work_hours_ui: {e}")
|
||||
|
||||
self.rating_work_hours = round(rating.working_hours, 2)
|
||||
self.rating_work_hours_lock = False
|
||||
|
||||
bpy.context.area.tag_redraw()
|
||||
|
||||
|
||||
# class RatingPropsCollection(PropertyGroup):
|
||||
# ratings = CollectionProperty(type = RatingProperties)
|
||||
Reference in New Issue
Block a user