Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions client_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,58 @@ def send_rating(asset_id: str, rating_type: str, rating_value: Union[str, int]):
)


def get_not_used_reasons():
"""Fetch the shared "didn't use it" reason choices."""
data = ensure_minimal_data()
with requests.Session() as session:
return session.get(
f"{get_base_url()}/ratings/get_not_used_reasons",
json=data,
timeout=TIMEOUT,
proxies=NO_PROXIES,
)


def get_didnt_use(asset_id: str):
"""Fetch the user's "I didn't use this asset" flag for one asset."""
data = ensure_minimal_data({"asset_id": asset_id})
with requests.Session() as session:
return session.get(
f"{get_base_url()}/ratings/get_didnt_use",
json=data,
timeout=TIMEOUT,
proxies=NO_PROXIES,
)


def send_didnt_use(
asset_id: str,
didnt_use: bool,
reason_id: Optional[int] = None,
replace_rating: bool = False,
):
"""Set (with an optional reason) or clear the "I didn't use this asset" flag.

replace_rating deletes the user's score ratings server-side instead of the
409 refusal - send it only from UI that warned about the replacement.
"""
data = ensure_minimal_data(
{
"asset_id": asset_id,
"didnt_use": didnt_use,
"reason_id": reason_id,
"replace_rating": replace_rating,
}
)
with requests.Session() as session:
return session.post(
f"{get_base_url()}/ratings/send_didnt_use",
json=data,
timeout=TIMEOUT,
proxies=NO_PROXIES,
)


# BOOKMARKS
def get_bookmarks():
data = ensure_minimal_data()
Expand Down
13 changes: 13 additions & 0 deletions datas.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,17 @@ class AssetRating(FromDictMixin):
quality_fetched: bool = False
working_hours: Optional[float] = None # name kept as comes from API
working_hours_fetched: bool = False
# The "I didn't use this asset" feedback flag; mutually exclusive with
# quality/working_hours server-side (rating wins).
didnt_use: bool = False
didnt_use_reason: Optional[str] = None
didnt_use_reason_id: Optional[int] = None
didnt_use_fetched: bool = False
# Session-local memory of the scores a "didn't use" pick replaced, so
# undo restores the numbers, not just the unflagged state.
didnt_use_replaced_quality: Optional[float] = None
didnt_use_replaced_working_hours: Optional[float] = None
# The last "didn't use" server refusal, shown inline under the control -
# the corner report overlay is out of sight of the rating popup.
didnt_use_error: Optional[str] = None
# TODO: Add last time ratings checked to improve caching
5 changes: 5 additions & 0 deletions global_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
}

RATINGS: dict[str, datas.AssetRating] = {}

# Shared "didn't use it" reason choices fetched from the server
# (None = not fetched yet, [] = fetch requested/failed - flag still works
# without a reason).
NOT_USED_REASONS: Optional[list[dict]] = None
BKIT_PROFILE: datas.MineProfile = datas.MineProfile()
"""Profile of the current user."""
BKIT_AUTHORS: dict[int, datas.UserProfile] = {}
Expand Down
164 changes: 163 additions & 1 deletion ratings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import logging

import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.props import BoolProperty, IntProperty, StringProperty
from bpy.types import Gizmo, GizmoGroup, Operator
from bpy_extras import view3d_utils
from mathutils import Matrix
Expand Down Expand Up @@ -125,6 +125,18 @@ def get_assets_for_rating():
)


def _flagged_hint(rating) -> str:
"""What the flag replaced, so undo's promise is concrete."""
replaced = []
if rating.didnt_use_replaced_quality:
replaced.append(f"{rating.didnt_use_replaced_quality:g}\u2605")
if rating.didnt_use_replaced_working_hours:
replaced.append(f"{rating.didnt_use_replaced_working_hours:g} h")
if replaced:
return f"Your {' / '.join(replaced)} rating was cleared - Undo restores it"
return "Marked as not used - undo below to rate"


def draw_ratings_menu(self, context, layout):
pcoll = icons.icon_collections["main"]

Expand Down Expand Up @@ -156,6 +168,18 @@ def draw_ratings_menu(self, context, layout):
if profile and len(profile.firstName) > 0:
profile_name = " " + profile.firstName

# Mutual exclusivity, drawn not explained: a flagged asset's rating
# controls are disabled (the server would refuse the score anyway -
# flag and rating can never both exist).
rating_state = ratings_utils.get_rating_local(self.asset_id)
flagged = rating_state is not None and rating_state.didnt_use
outer = col
if flagged:
row.label(text=_flagged_hint(rating_state), icon="INFO")
col = col.column()
col.enabled = not flagged
row = col.row()

row.label(text="Rate Quality:", icon="SOLO_ON")
# row = col.row()
# row.label(text='Please help the community by rating quality:')
Expand Down Expand Up @@ -203,6 +227,40 @@ def draw_ratings_menu(self, context, layout):
row = col.row()
row.label(text=f"Thanks{profile_name}, you are amazing!", icon="FUND")

draw_didnt_use_control(outer, self.asset_id)


# The NotUsedMenu target: Blender menus draw without arguments, so the last
# ratings UI drawn for an asset parks its id here for the menu to read.
active_not_used_asset_id = ""


def draw_didnt_use_control(layout, asset_id):
"""The "Didn't use it" flag control: one dropdown for every state,
like on the My downloads page. Never disabled: on a rated asset the menu
warns that a pick replaces the rating, and undo restores it."""
global active_not_used_asset_id
active_not_used_asset_id = asset_id
ratings_utils.ensure_not_used_reasons()
ratings_utils.ensure_didnt_use(asset_id)

rating = ratings_utils.get_rating_local(asset_id)
flagged = rating is not None and rating.didnt_use

layout.separator()
row = layout.row()
if flagged:
text = rating.didnt_use_reason or "I didn't use this asset"
row.menu(NotUsedMenu.bl_idname, text=text, icon="CHECKMARK")
else:
row.menu(NotUsedMenu.bl_idname, text="I didn't use this asset")
if rating is not None and rating.didnt_use_error:
# The refusal right under the control that caused it - the corner
# report overlay is out of sight of this popup.
error_row = layout.row()
error_row.alert = True
error_row.label(text=rating.didnt_use_error, icon="ERROR")


class FastRateMenu(Operator, ratings_utils.RatingProperties):
"""Rating of the assets, also directly from the asset bar - without need to download assets"""
Expand Down Expand Up @@ -252,6 +310,13 @@ def execute(self, context):
self.asset_type = self.asset_data["assetType"]
elif ui_props.active_index > -1:
sr = search.get_search_results()
if ui_props.active_index >= len(sr):
bk_logger.warning(
"FastRateMenu: active_index %d out of bounds for search results of length %d",
ui_props.active_index,
len(sr),
)
return {"CANCELLED"}
self.asset_data = dict(sr[ui_props.active_index])
self.asset_id = self.asset_data["id"]
self.asset_type = self.asset_data["assetType"]
Expand Down Expand Up @@ -479,9 +544,106 @@ def refresh(self, context):
gz.matrix_basis = Matrix.Translation(loc) @ R @ Matrix.Diagonal(scale.to_4d())


class SetNotUsed(bpy.types.Operator):
"""Mark the asset as one you did not use, or undo that.\nMutually exclusive with rating - the flag is refused while your rating stands"""

bl_idname = "wm.blenderkit_not_used"
bl_label = "I didn't use this asset"
bl_options = {"REGISTER", "INTERNAL"}

asset_id: StringProperty( # type: ignore[valid-type]
name="Asset Base Id",
description="Unique id of the asset (hidden)",
default="",
options={"SKIP_SAVE"},
)
reason_id: IntProperty( # type: ignore[valid-type]
name="Reason",
description="Server id of the picked reason; -1 means no particular reason",
default=-1,
options={"SKIP_SAVE"},
)
undo: BoolProperty( # type: ignore[valid-type]
name="Undo",
description="Clear the flag - I did use it after all",
default=False,
options={"SKIP_SAVE"},
)

def execute(self, context):
ratings_utils.store_didnt_use_error(self.asset_id, None)
if self.undo:
# A pick that replaced a rating undoes through the rating API -
# re-rating clears the flag server-side, numbers included.
if not ratings_utils.restore_replaced_scores(self.asset_id):
client_lib.send_didnt_use(self.asset_id, False)
else:
reason_id = self.reason_id if self.reason_id >= 0 else None
# Consent came from the menu: its header warns "This replaces
# your rating" whenever a rating stands.
ratings_utils.remember_replaced_scores(self.asset_id)
client_lib.send_didnt_use(
self.asset_id, True, reason_id, replace_rating=True
)
return {"FINISHED"}


class NotUsedMenu(bpy.types.Menu):
"""Reason picker for the "Didn't use it" flag - one click saves,
picking another reason just changes it, undo lives at the bottom."""

bl_idname = "OBJECT_MT_blenderkit_not_used"
bl_label = "Why didn't you use it?"

def draw(self, context):
layout = self.layout
asset_id = active_not_used_asset_id
rating = ratings_utils.get_rating_local(asset_id)
flagged = rating is not None and rating.didnt_use
rated = rating is not None and (
bool(rating.quality) or bool(rating.working_hours)
)
current_reason_id = rating.didnt_use_reason_id if flagged else None

if rated and not flagged:
# The consequence, stated where the eyes already are - picking a
# reason below replaces the rating (undo restores it).
layout.label(text="This replaces your rating", icon="INFO")
layout.separator()

op = layout.operator(
SetNotUsed.bl_idname,
text="No particular reason",
icon="CHECKMARK" if flagged and current_reason_id is None else "NONE",
)
op.asset_id = asset_id
for reason in global_vars.NOT_USED_REASONS or []:
op = layout.operator(
SetNotUsed.bl_idname,
text=reason["label"],
icon="CHECKMARK" if reason["id"] == current_reason_id else "NONE",
)
op.asset_id = asset_id
op.reason_id = reason["id"]
if flagged:
layout.separator()
has_memory = bool(
rating.didnt_use_replaced_quality
or rating.didnt_use_replaced_working_hours
)
undo_text = (
"Undo - restore my rating" if has_memory else "Undo - I did use it"
)
op = layout.operator(SetNotUsed.bl_idname, text=undo_text, icon="X")
op.asset_id = asset_id
op.undo = True


classes = (
FastRateMenu,
SetBookmark,
SetNotUsed,
NotUsedMenu,
RatingStarWidget,
RatingStarWidgetGroup,
ratings_utils.RatingProperties,
Expand Down
Loading
Loading