Compare commits

...

7 Commits

Author SHA1 Message Date
Dogukan Karatas b05447dc30 Merge pull request #308 from specklesystems/dogukan/ssl-match-localhost
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: skip ssl use for http servers
2026-01-20 09:44:30 +01:00
Dogukan Karatas 7a36f9ec08 check url 2026-01-20 09:29:14 +01:00
Mucahit Bilal GOKER 80e3971706 Show update button in connector ui (#297)
* bump specklepy

* update button first pass

* clear timer on unregister

* remove unnecessary specklepy import handling

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2026-01-19 14:06:07 +03:00
Dogukan Karatas dc770b7a79 Merge pull request #307 from specklesystems/dogukan/cnx-1776-typeerror-bpy_prop_collection__contains__-expected-a-string
fix: handle null material name
2026-01-08 18:09:41 +01:00
Dogukan Karatas f8e7d391be handle none 2026-01-08 17:59:06 +01:00
Mucahit Bilal GOKER 3092ba3056 separate panel for model cards (#292)
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2026-01-08 16:05:54 +03:00
Mucahit Bilal GOKER 9d10006116 bump specklepy (#306) 2026-01-06 12:41:32 +03:00
9 changed files with 249 additions and 75 deletions
+27
View File
@@ -34,6 +34,8 @@ bl_info = {
# UI
from .connector.ui.main_panel import SPECKLE_PT_main_panel
from .connector.ui.update_panel import SPECKLE_PT_update_panel
from .connector.ui.model_cards_panel import SPECKLE_PT_model_cards_panel
from .connector.utils.account_manager import speckle_workspace
from .connector.ui.project_selection_dialog import (
SPECKLE_OT_project_selection_dialog,
@@ -80,6 +82,8 @@ from .connector.blender_operators.add_project_by_url import (
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
from .connector.blender_operators.version_check import SPECKLE_OT_version_check
from .connector.blender_operators.update_button import SPECKLE_OT_update_button
from .connector.utils.account_manager import (
speckle_account,
get_default_account_id,
@@ -105,6 +109,14 @@ from .connector.ui.account_selection_dialog import (
)
def delayed_version_check():
"""Timer function to check for updates after addon startup"""
try:
bpy.ops.speckle.version_check()
except Exception as e:
print(f"[Speckle] Failed to check for updates: {e}")
def invoke_window_manager_properties():
# Accounts
WindowManager.speckle_accounts = bpy.props.CollectionProperty(type=speckle_account)
@@ -139,11 +151,17 @@ def invoke_window_manager_properties():
)
# Objects
WindowManager.speckle_objects = bpy.props.CollectionProperty(type=speckle_object)
# Update checking
WindowManager.update_available = bpy.props.BoolProperty(default=False)
WindowManager.latest_version = bpy.props.StringProperty(default="")
WindowManager.update_url = bpy.props.StringProperty(default="")
# Classes to load
classes = (
SPECKLE_PT_update_panel,
SPECKLE_PT_main_panel,
SPECKLE_PT_model_cards_panel,
SPECKLE_OT_publish,
SPECKLE_OT_load,
SPECKLE_OT_project_selection_dialog,
@@ -171,6 +189,8 @@ classes = (
SPECKLE_OT_add_project_by_url,
SPECKLE_OT_create_project,
SPECKLE_OT_create_model,
SPECKLE_OT_version_check,
SPECKLE_OT_update_button,
speckle_account,
SPECKLE_UL_workspaces_list,
SPECKLE_OT_workspace_selection_dialog,
@@ -203,8 +223,15 @@ def register():
except Exception as e:
print(f"[Speckle] Failed to pre-warm client: {e}")
# Use a timer to delay the version check
bpy.app.timers.register(delayed_version_check, first_interval=2.0)
def unregister():
# Clear any pending timers to prevent duplicate calls
if bpy.app.timers.is_registered(delayed_version_check):
bpy.app.timers.unregister(delayed_version_check)
icons.unload_icons()
unregister_speckle_state() # Unregister SpeckleState
_client_cache.clear()
@@ -0,0 +1,27 @@
import bpy
import webbrowser
from bpy.types import Context
class SPECKLE_OT_update_button(bpy.types.Operator):
"""Operator for opening the download URL for the latest Speckle Blender connector"""
bl_idname = "speckle.update_button"
bl_label = "Update"
bl_description = "Download the latest version of the Speckle Blender connector"
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if not wm.update_url:
self.report({"ERROR"}, "No update URL available")
return {"CANCELLED"}
try:
webbrowser.open(wm.update_url)
self.report({"INFO"}, f"Opening download page for v{wm.latest_version}")
except Exception as e:
self.report({"ERROR"}, f"Failed to open download page: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
@@ -0,0 +1,51 @@
import bpy
from bpy.types import Context
from specklepy.core.api.connector_versions import get_latest_version
# Get current version from bl_info
from ... import bl_info
class SPECKLE_OT_version_check(bpy.types.Operator):
"""Operator for checking if a newer version of the Speckle Blender connector is available"""
bl_idname = "speckle.version_check"
bl_label = "Check for Updates"
bl_description = (
"Check if a newer version of the Speckle Blender connector is available"
)
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
# Reset previous state
wm.update_available = False
wm.latest_version = ""
wm.update_url = ""
try:
current_version = bl_info["version"]
current_version_str = (
f"{current_version[0]}.{current_version[1]}.{current_version[2]}"
)
# Get latest version info
latest_version_info = get_latest_version("blender", False)
latest_version_str = latest_version_info.number # semantic version string
# Compare versions - if they're different, show update
if latest_version_str != current_version_str:
wm.update_available = True
wm.latest_version = latest_version_str
wm.update_url = str(
latest_version_info.url
) # Convert HttpUrl to string
self.report({"INFO"}, f"Update available: v{latest_version_str}")
else:
self.report({"INFO"}, "You have the latest version")
except Exception as e:
error_msg = f"Failed to check for updates: {str(e)}"
self.report({"ERROR"}, error_msg)
return {"FINISHED"}
-67
View File
@@ -104,70 +104,3 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
row = layout.row()
row.enabled = project_selected and model_selected and version_selected
row.operator("speckle.load", text="Load Model", icon="IMPORT")
layout.separator()
# group model cards by project name
project_groups = {}
for model_card in context.scene.speckle_state.model_cards:
project_name = (
model_card.project_name if model_card.project_name else "No Project"
)
if project_name not in project_groups:
project_groups[project_name] = []
project_groups[project_name].append(model_card)
for project_name, model_cards in project_groups.items():
project_box = layout.box()
project_row = project_box.row()
project_row.label(text=f"Project: {project_name}", icon="TRIA_RIGHT")
for model_card in model_cards:
box: UILayout = project_box.box()
row_1: UILayout = box.row()
row_2: UILayout = box.row()
if model_card.is_publish:
# Publish button in the model card
row_1.operator(
"speckle.model_card_publish", text="", icon="EXPORT"
).model_card_id = model_card.get_model_card_id()
# Selection filter button in the model card
row_2.operator(
"speckle.selection_filter_dialog",
text=f"Selection: {len(model_card.objects)} objects",
).model_card_id = model_card.get_model_card_id()
elif not model_card.is_publish:
# Load button in the model card
row_1.operator(
"speckle.model_card_load", text="", icon="IMPORT"
).model_card_id = model_card.get_model_card_id()
version_button_text = (
f"Latest: {model_card.version_id}"
if model_card.load_option == "LATEST"
else f"{model_card.version_id}"
)
row_2.operator(
"speckle.version_selection_dialog",
text=version_button_text,
).model_card_id = model_card.get_model_card_id()
# TODO: Get last updated time
else:
print({"ERROR"}, "Model card state unknown")
return
row_1.label(text=f"{model_card.model_name}")
# Select button in the model card
select_op = row_1.operator(
"speckle.select_objects",
text="",
icon_value=get_icon("object_highlight"),
)
select_op.model_card_id = model_card.get_model_card_id()
# Settings button in the model card
row_1.operator(
"speckle.model_card_settings", text="", icon="COLLAPSEMENU"
).model_card_id = model_card.get_model_card_id()
@@ -0,0 +1,89 @@
import bpy
from bpy.types import UILayout, Context
from .icons import get_icon
class SPECKLE_PT_model_cards_panel(bpy.types.Panel):
"""
Panel for displaying Speckle model cards.
"""
bl_label = "Model Cards"
bl_idname = "SPECKLE_PT_model_cards_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Speckle"
bl_order = 1
@classmethod
def poll(cls, context: Context) -> bool:
"""Only show panel when model cards exist"""
return bool(context.scene.speckle_state.model_cards)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
# group model cards by project name
project_groups = {}
for model_card in context.scene.speckle_state.model_cards:
project_name = (
model_card.project_name if model_card.project_name else "No Project"
)
if project_name not in project_groups:
project_groups[project_name] = []
project_groups[project_name].append(model_card)
for project_name, model_cards in project_groups.items():
project_box = layout.box()
project_row = project_box.row()
project_row.label(text=f"Project: {project_name}", icon="TRIA_RIGHT")
for model_card in model_cards:
box: UILayout = project_box.box()
row_1: UILayout = box.row()
row_2: UILayout = box.row()
if model_card.is_publish:
# Publish button in the model card
row_1.operator(
"speckle.model_card_publish", text="", icon="EXPORT"
).model_card_id = model_card.get_model_card_id()
# Selection filter button in the model card
row_2.operator(
"speckle.selection_filter_dialog",
text=f"Selection: {len(model_card.objects)} objects",
).model_card_id = model_card.get_model_card_id()
elif not model_card.is_publish:
# Load button in the model card
row_1.operator(
"speckle.model_card_load", text="", icon="IMPORT"
).model_card_id = model_card.get_model_card_id()
version_button_text = (
f"Latest: {model_card.version_id}"
if model_card.load_option == "LATEST"
else f"{model_card.version_id}"
)
row_2.operator(
"speckle.version_selection_dialog",
text=version_button_text,
).model_card_id = model_card.get_model_card_id()
# TODO: Get last updated time
else:
print({"ERROR"}, "Model card state unknown")
return
row_1.label(text=f"{model_card.model_name}")
# Select button in the model card
select_op = row_1.operator(
"speckle.select_objects",
text="",
icon_value=get_icon("object_highlight"),
)
select_op.model_card_id = model_card.get_model_card_id()
# Settings button in the model card
row_1.operator(
"speckle.model_card_settings", text="", icon="COLLAPSEMENU"
).model_card_id = model_card.get_model_card_id()
+48
View File
@@ -0,0 +1,48 @@
import bpy
from bpy.types import UILayout, Context
class SPECKLE_PT_update_panel(bpy.types.Panel):
"""Panel for displaying connector update notifications"""
bl_label = "Update Speckle"
bl_idname = "SPECKLE_PT_update_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Speckle"
bl_order = 0 # This ensures it appears above the main panel
@classmethod
def poll(cls, context: Context) -> bool:
"""Only show this panel when an update is available"""
wm = context.window_manager
return getattr(wm, "update_available", False)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
# Get current version from bl_info
from ... import bl_info
current_version = bl_info["version"]
current_version_str = (
f"{current_version[0]}.{current_version[1]}.{current_version[2]}"
)
# Update notification
box = layout.box()
box.alert = True # Makes the box stand out with alert styling
col = box.column()
col.label(text="New version available!", icon="INFO")
row = col.row()
row.label(text=f"Current: v{current_version_str}")
row = col.row()
row.label(text=f"Latest: v{wm.latest_version}")
# Update button
row = col.row()
row.operator("speckle.update_button", text="Download Update", icon="LINKED")
@@ -1,6 +1,7 @@
import bpy
from specklepy.core.api.credentials import get_local_accounts
from typing import List, Tuple, Optional, Dict
from urllib.parse import urlparse
from specklepy.core.api.credentials import Account
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.wrapper import StreamWrapper
@@ -23,7 +24,9 @@ class SpeckleClientCache:
if not account:
raise ValueError(f"No account found for ID: {account_id}")
client = SpeckleClient(host=account.serverInfo.url)
url = account.serverInfo.url
use_ssl = urlparse(url).scheme.lower() != "http"
client = SpeckleClient(host=url, use_ssl=use_ssl)
client.authenticate_with_account(account)
self._clients[account_id] = client
return client
+1 -1
View File
@@ -695,7 +695,7 @@ def render_material_proxy_to_native(
continue
render_material = proxy.value
material_name = getattr(render_material, "name", "Material")
material_name = getattr(render_material, "name", None) or "Material"
# create or get existing material
blender_material = create_material_from_proxy(render_material, material_name)
+2 -6
View File
@@ -5,12 +5,8 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.4",
"specklepy>=3.2.3",
]
[dependency-groups]
dev = [
"fake-bpy-module-latest>=20240524,<20240525",
"ruff>=0.4.4,<0.5",
]
dev = ["fake-bpy-module-latest>=20240524,<20240525", "ruff>=0.4.4,<0.5"]