From 9fe38ce58a8d09aec9bf2533315500f0ff48b8b0 Mon Sep 17 00:00:00 2001 From: Nero WTD Date: Tue, 27 Oct 2026 11:45:17 +0100 Subject: [PATCH] Scene Manager: request publishes missing on this site - Request (globe) on casting rows and loaded assets whose latest publish is not on this site, and Request Missing for all of them, through the new wtdcmds command wtd_request_revisions. Requested publishes show "requested, waiting for the download". - The version picker requests a version that is not on this site. - A 404 explains that the session is gone or wtdcmds is too old. - Fake server: car (vehicles) with a publish not on this site, wtd_request_revisions and /_test/download; 45 checks. - Tested live: car_paul modeling.blend v001 -> Requested. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 9 ++-- README.md | 12 +++-- dev/fake_libreflow.py | 32 +++++++++-- dev/make_test_assets.py | 2 + dev/test_scene_manager.py | 36 +++++++++++-- scene_manager/ipc.py | 1 + scene_manager/operators.py | 107 +++++++++++++++++++++++++++++++++++-- ui/panel.py | 17 ++++-- 8 files changed, 197 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c471fca..6176f17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,8 @@ that contains this repo on `sys.path` and importing the repo folder name (see 1. `blender -b --factory-startup --python dev/make_test_assets.py -- ` 2. `python dev/fake_libreflow.py ` (keep running; restart for a clean state) 3. `blender -b --factory-startup --python-exit-code 1 --python dev/test_scene_manager.py -- ` - (36 checks, "All checks passed"). + (45 checks, "All checks passed"). `/_test/publish` and `/_test/download` simulate a new + publish and a requested revision reaching this site. The fake server follows the real `wtdcmds` replies; check them against the real Libreflow when one is available. @@ -83,8 +84,10 @@ that contains this repo on `sys.path` and importing the repo folder name (see - Set dressing publishes (`setdress_low`) hold a whole layout scene: the set is the `environments` collection, next to `lights`, `cameras`, `characters`, `_unused`. - Tested live: connection, context, casting, load (instance and override), version switch - and update, remove, assetize errors. Not tested: an actual assetize publish (it writes - to production). + and update, remove, assetize errors, one request (`car_paul` modeling.blend v001 → + `Requested`). Not tested: an actual assetize publish (it writes to production). + New wtdcmds commands need every Libreflow session restarted (a running one keeps the + old code and answers 404). ## Open points diff --git a/README.md b/README.md index 2b89958..c341bf8 100644 --- a/README.md +++ b/README.md @@ -346,8 +346,11 @@ Lists the assets cast in the shot, with how many instances are loaded out of how | ✔ | Loaded. | | Import arrow | Not loaded (or fewer instances than cast): click it to load this asset only. | | ⚠ red | Cannot be loaded, with the reason from Libreflow: not found in the flow, no published file, or nothing available on this site yet. | +| Globe | The latest publish is not on this site: click it to request it (see below). | +| Hourglass | Requested, waiting for the download. | - **Load Missing** links every cast asset that is not loaded yet, and adds the missing instances. Running it again only adds what is missing. +- **Request Missing** (shown when needed) asks Libreflow to download, on this site, the latest publish of every cast or loaded asset that is not here yet: the same as **Request** in Libreflow (from the exchange site, or from the site that published it). The download is done by the Libreflow sync: click **Refresh** later, then load or update. - **Refresh** reads the casting and the versions again. - **Loaded, no longer cast** lists assets loaded earlier that the shot's casting no longer contains. @@ -364,9 +367,9 @@ All of these can be changed in the [Configuration](#configuration). Lists everything the Scene Manager loaded, one line per asset, with its version: -- The version is **red** when it is not the latest publish. The line under it says either "v004 available" (click the update icon), or "v004 published, not available on this site yet" (wait for the download). +- The version is **red** when it is not the latest publish. The line under it says either "v004 available" (click the update icon), or "v004 published, not available on this site yet" (click the globe to request it, then "v004 requested, waiting for the download"). - **Arrow**: shows the asset's instances, each with a button to select its objects and one to remove it. -- **Menu icon**: pick another published version, for example to roll back. Versions not on this site are marked and cannot be picked. +- **Menu icon**: pick another published version, for example to roll back. Versions not on this site are marked: picking one requests it; pick it again once it is downloaded. - **✕**: removes the asset entirely: all its instances and the link to its file. - **Update All** / **Update Selected**: switch all the loaded assets (or the ones of the selected objects) to their latest available publish. - **Remove Selected**: removes the instances of the selected objects. @@ -463,7 +466,10 @@ Start Libreflow (with the `wtdcmds` extension), then click the refresh icon of t Its red line says why. "No file matching..." means none of its files matches `SCENE_MANAGER_FILE_PATTERNS`. "No collection named..." means the file has none of `COL_`, `` or `environments`, and several top-level collections: rename the published collection, or add its name to `SCENE_MANAGER_COLLECTION_NAMES`. **Scene Manager: the version stays red after Update.** -The newer version is published but not downloaded on this site yet. Wait for the sync, then update again. +The newer version is published but not downloaded on this site yet. Request it (globe icon) if it is not requested yet, wait for the sync, then refresh and update again. + +**Scene Manager: "...its wtdcmds extension is too old".** +Libreflow runs an older `wtdcmds` without the command (e.g. requests). Update the extension and restart Libreflow. **Shot Viewer: playback only plays one shot.** **Loop Shot** is on. Untick it to play the whole sequence. diff --git a/dev/fake_libreflow.py b/dev/fake_libreflow.py index 33d0873..b0d6eed 100644 --- a/dev/fake_libreflow.py +++ b/dev/fake_libreflow.py @@ -3,11 +3,12 @@ python dev/fake_libreflow.py Serves a project with one shot casting bob (main_character, x2), tree (bg_set), rock (main_props), -lamp (main_props, nothing published) and ghost (not in the flow). The asset files come +lamp (main_props, nothing published), car (vehicles, v001 not on this site) and ghost (not in the flow). The asset files come from dev/make_test_assets.py (/lib). Writes /kabaret/server.json: point config LIBREFLOW_SERVER_FILE to it (see dev/test_scene_manager.py). Test-only endpoints: POST /_test/publish {file_oid, name, available, path}, +POST /_test/download {revision_oid} (a requested revision reaches this site), POST /_test/sessions {uid: session}, POST /_test/calls (commands received). State lives in memory: restart the server for a clean state. """ @@ -33,10 +34,22 @@ ASSETS = { # asset oid -> (type, {file_oid: (label, [revisions oldest first])}) f"{PROJECT}/asset_types/main_props/assets/rock": ("main_props", {f"{PROJECT}/asset_types/main_props/assets/rock/tasks/modeling/files/rock_blend": ("modeling/rock.blend", [ rev("v001", f"{ROOT}/lib/rock/v001/rock.blend")])}), f"{PROJECT}/asset_types/main_props/assets/lamp": ("main_props", {}), + f"{PROJECT}/asset_types/vehicles/assets/car": ("vehicles", {f"{PROJECT}/asset_types/vehicles/assets/car/tasks/rigging/files/car_blend": ("rigging/car.blend", [ + rev("v001", f"{ROOT}/lib/car/v001/car.blend", available=False)])}), } -CASTING = [("bob", "main_character", 2), ("tree", "bg_set", 1), ("rock", "main_props", 1), ("lamp", "main_props", 1), ("ghost", "main_character", 1)] +CASTING = [("bob", "main_character", 2), ("tree", "bg_set", 1), ("rock", "main_props", 1), ("lamp", "main_props", 1), ("car", "vehicles", 1), ("ghost", "main_character", 1)] CALLS = [] +def set_oids(): + for _a, (_t, files) in ASSETS.items(): + for f, (_l, revs) in files.items(): + for r in revs: r["oid"] = f"{f}/history/revisions/{r['name']}" + +def find_rev(oid): + return next((r for _a, (_t, files) in ASSETS.items() for _l, revs in files.values() for r in revs if r["oid"] == oid), None) + +set_oids() + def file_data(file_oid): for asset_oid, (atype, files) in ASSETS.items(): if file_oid in files: @@ -98,6 +111,15 @@ def cmd(name, kw): if kw["file_oid"] in files: return list(reversed(files[kw["file_oid"]][1])) raise ValueError("No file") + if name == "wtd_request_revisions": + res = {} + for oid in kw["revision_oids"]: + r = find_rev(oid) + if r is None: + res[oid] = {"oid": oid, "error": f"Could not find {oid}"}; continue + if not r["available"]: r["sync_status"] = "Requested" + res[oid] = r + return res if name == "wtd_assetize": src = kw["source_path"] if not os.path.isfile(src): raise ValueError(f"Source file {src} does not exist") @@ -115,7 +137,7 @@ def cmd(name, kw): label, revs = files.setdefault(file_oid, (f"{task}/{fname}.blend", [])) name = f"v{len(revs) + 1:03d}" dest = f"{ROOT}/lib/{aname}/{name}/{fname}.blend"; os.makedirs(os.path.dirname(dest), exist_ok=True); shutil.copy(src, dest) - revs.append(rev(name, dest, comment=kw.get("comment", ""))) + revs.append(rev(name, dest, comment=kw.get("comment", ""))); set_oids() return {"asset_oid": asset_oid, "asset_created": created, **file_data(file_oid)} raise KeyError(name) @@ -139,6 +161,10 @@ class Handler(BaseHTTPRequestHandler): f = body["file_oid"] for _a, (_t, files) in ASSETS.items(): if f in files: files[f][1].append(rev(body["name"], body.get("path", "/nowhere/" + body["name"]), body.get("available", True))) + set_oids() + return self.reply(200, {"result": True}) + if url.path == "/_test/download": + r = find_rev(body["revision_oid"]); r["available"] = True; r["sync_status"] = "Available" return self.reply(200, {"result": True}) if url.path == "/_test/sessions": SESSIONS.clear(); SESSIONS.update(body); return self.reply(200, {"result": True}) diff --git a/dev/make_test_assets.py b/dev/make_test_assets.py index e05f8f9..db1ea55 100644 --- a/dev/make_test_assets.py +++ b/dev/make_test_assets.py @@ -4,6 +4,7 @@ bob: a rig (COL_bob: armature + meshes), v001 has 1 mesh, v002 has 2. tree: COL_tree, plus an unrelated top-level collection. +car: COL_car. rock: a single top-level collection not named after the asset (fallback case), its mesh using a node group linked from Blender's essentials (an indirect library). """ @@ -56,4 +57,5 @@ write(os.path.join(lib, "bob", "v001", "rig.blend"), "COL_bob", meshes=1, rig=Tr write(os.path.join(lib, "bob", "v002", "rig.blend"), "COL_bob", meshes=2, rig=True, extra=True) write(os.path.join(lib, "tree", "v001", "model.blend"), "COL_tree", extra=True) write(os.path.join(lib, "rock", "v001", "rock.blend"), "rock_geo", node_group=True) +write(os.path.join(lib, "car", "v001", "car.blend"), "COL_car") print("Test assets written in", lib) diff --git a/dev/test_scene_manager.py b/dev/test_scene_manager.py index efd4160..ed0198d 100644 --- a/dev/test_scene_manager.py +++ b/dev/test_scene_manager.py @@ -33,6 +33,8 @@ SERVER_FILE = os.path.join(WORKDIR, "kabaret", "server.json") config.LIBREFLOW_SERVER_FILE = SERVER_FILE SHOT = "/melvile/films/f01/sequences/s010/shots/p010" BOB_FILE = "/melvile/asset_types/main_character/assets/bob/tasks/rigging/files/rig_blend" +CAR = "/melvile/asset_types/vehicles/assets/car" +CAR_FILE = CAR + "/tasks/rigging/files/car_blend" FAILED = [] @@ -92,7 +94,7 @@ scene.wtd_scene_manager.context_oid = SHOT pump() check("shot context validated", sm.context_label(SHOT) == ("shot: f01 / s010 / p010", True)) errors = {a["name"]: a["error"] for a in sm.STATE.casting.get(SHOT, {}).get("assets", [])} -check("casting read", set(errors) == {"bob", "tree", "rock", "lamp", "ghost"}, str(errors)) +check("casting read", set(errors) == {"bob", "tree", "rock", "lamp", "car", "ghost"}, str(errors)) print("Load") bpy.ops.wtd_scene.load() @@ -112,12 +114,31 @@ bpy.ops.wtd_scene.load() pump() check("load again adds nothing", message().startswith("Nothing new to load"), message()) +print("Request") +check("car can be requested", sm.missing_files(scene) == [CAR_FILE], str(sm.missing_files(scene))) +bpy.ops.wtd_scene.request() +pump() +check("car v001 requested", message() == "Requested car v001", message()) +check("nothing left to request", sm.missing_files(scene) == []) +calls = fake("/_test/calls", {}) +check("request sent to Libreflow", ["wtd_request_revisions", {"revision_oids": [CAR_FILE + "/history/revisions/v001"]}] in calls) +fake("/_test/download", {"revision_oid": CAR_FILE + "/history/revisions/v001"}) +bpy.ops.wtd_scene.refresh() +pump() +bpy.ops.wtd_scene.load(asset_oid=CAR) +pump() +check("car loaded once downloaded", loaded().get("car", (None,))[0] == "v001", message()) + print("Versions") check("bob is the latest", sm.version_status(bpy.data.libraries["bob"])[0] == "LATEST") fake("/_test/publish", {"file_oid": BOB_FILE, "name": "v003", "available": False}) sm.refresh_versions() pump() check("v003 not on this site: pending", sm.version_status(bpy.data.libraries["bob"])[0] == "PENDING") +bpy.ops.wtd_scene.request(file_oid=BOB_FILE) +pump() +status = sm.version_status(bpy.data.libraries["bob"]) +check("v003 requested", status == ("PENDING", "v003 requested, waiting for the download"), str(status)) fake( "/_test/publish", {"file_oid": BOB_FILE, "name": "v004", "available": True, "path": os.path.join(WORKDIR, "lib", "bob", "v001", "rig.blend")}, @@ -137,6 +158,13 @@ sm._REVISION_ITEMS[:] = [(r["name"], r["name"], "", "NONE", i) for i, r in enume bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v002") check("rolled back to v002", loaded()["bob"][0] == "v002") check("unavailable v003 refused", bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v003") == {"CANCELLED"}) +fake("/_test/publish", {"file_oid": BOB_FILE, "name": "v005", "available": False}) +sm.STATE.revisions[BOB_FILE] = ipc.call("wtd_get_revisions", None, file_oid=BOB_FILE) +sm._REVISION_ITEMS[:] = [(r["name"], r["name"], "", "NONE", i) for i, r in enumerate(sm.STATE.revisions[BOB_FILE])] +bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v005") +pump() +check("picking a version not on this site requests it", message() == "Requested bob v005", message()) +check("still on v002", loaded()["bob"][0] == "v002") print("Remove") bpy.ops.wtd_scene.remove(instance="COL_bob.001") @@ -148,10 +176,10 @@ bpy.ops.wtd_scene.remove(selected=True) check("selected instance removed", loaded()["tree"][1] == []) bpy.ops.wtd_scene.remove("EXEC_DEFAULT", library="rock") check("whole asset removed", "rock" not in bpy.data.libraries) -check("its indirect library removed too", set(bpy.data.libraries.keys()) == {"bob", "tree"}, str(bpy.data.libraries.keys())) +check("its indirect library removed too", set(bpy.data.libraries.keys()) == {"bob", "car", "tree"}, str(bpy.data.libraries.keys())) bpy.ops.wtd_scene.load() pump() -check("load restores what is missing", {k: len(v[1]) for k, v in loaded().items()} == {"bob": 2, "rock": 1, "tree": 1}) +check("load restores what is missing", {k: len(v[1]) for k, v in loaded().items()} == {"bob": 2, "car": 1, "rock": 1, "tree": 1}) print("Ambiguous file") try: @@ -188,7 +216,7 @@ path = os.path.join(WORKDIR, "scene_manager_test.blend") bpy.ops.wm.save_as_mainfile(filepath=path) bpy.ops.wm.open_mainfile(filepath=path) sm._on_load_post() -check("everything found after reopening", set(loaded()) == {"bob", "lamp", "rock", "tree"}) +check("everything found after reopening", set(loaded()) == {"bob", "car", "lamp", "rock", "tree"}) sm.connect(bpy.context.scene) pump() check("versions checked again", sm.version_status(bpy.data.libraries["bob"])[0] == "UPDATE") diff --git a/scene_manager/ipc.py b/scene_manager/ipc.py index 6344961..cd11ad5 100644 --- a/scene_manager/ipc.py +++ b/scene_manager/ipc.py @@ -24,6 +24,7 @@ from .. import config # Hints added to the server's message for the statuses the user can act on. _STATUS_HINTS = { 401: "the server refused the token, try again", + 404: "the Libreflow session is gone, or its wtdcmds extension is too old: update it and restart Libreflow", 504: "no answer within 10 s, the command may still complete in Libreflow", } # Statuses whose message replaces the server's. diff --git a/scene_manager/operators.py b/scene_manager/operators.py index 0847e72..40caa9b 100644 --- a/scene_manager/operators.py +++ b/scene_manager/operators.py @@ -120,9 +120,36 @@ def version_status(library): return "LATEST", "latest" if available is not None and available["name"] != current: return "UPDATE", f"{available['name']} available" + if latest.get("sync_status") == "Requested": + return "PENDING", f"{latest['name']} requested, waiting for the download" return "PENDING", f"{latest['name']} published, not available on this site yet" +def file_data(file_oid, default=None): + """Latest known revisions of a file (refreshed by requests and version checks).""" + return STATE.files.get(file_oid) or default + + +def to_request(file): + """The latest revision of a file when it is not on this site and not requested yet, else None.""" + latest = (file or {}).get("latest") + if latest and latest.get("oid") and not latest.get("available") and latest.get("sync_status") != "Requested": + return latest + return None + + +def missing_files(scene): + """File oids of the cast and loaded assets whose latest publish can be requested.""" + casting = STATE.casting.get(context_oid(scene)) or {"assets": []} + file_oids = [a["file"]["file_oid"] for a in casting["assets"] if a.get("file")] + file_oids += [library["wtd_file_oid"] for library in lib.managed_libraries()] + return [oid for oid in dict.fromkeys(file_oids) if to_request(file_data(oid))] + + +def _asset_of(file_oid): + return file_oid.split("/assets/", 1)[-1].split("/", 1)[0] + + # --------------------------------------------------------------------------- # # Background requests # --------------------------------------------------------------------------- # @@ -225,6 +252,41 @@ def refresh_versions(quiet=False): ) +def request_revisions(revisions): + """Ask Libreflow to download revisions on this site ({revision oid: file oid}).""" + uid = session_uid() + + def done(result): + requested, messages = [], [] + for oid, data in result.items(): + file_oid = revisions.get(oid, "") + what = f"{_asset_of(file_oid)} {data.get('name') or oid.rsplit('/', 1)[-1]}" + if data.get("error"): + messages.append(f"{what}: {data['error']}") + continue + file = STATE.files.get(file_oid) + if file is not None and (file.get("latest") or {}).get("oid") == oid: + file["latest"] = data + if data.get("available"): + file["latest_available"] = data + for revision in STATE.revisions.get(file_oid, []): + if revision.get("oid") == oid: + revision.update(data) + if data.get("available"): + messages.append(f"{what} is already on this site: refresh") + else: + requested.append(what) + text = f"Requested {', '.join(requested)}" if requested else "Nothing requested" + report(text + ("; " + "; ".join(messages) if messages else ""), "WARNING" if messages else "INFO") + + ipc.run( + "Requesting", + lambda: ipc.call("wtd_request_revisions", uid, revision_oids=list(revisions)), + done, + _on_error("Request"), + ) + + # --------------------------------------------------------------------------- # # Settings # --------------------------------------------------------------------------- # @@ -486,6 +548,34 @@ class WTD_OT_scene_update(bpy.types.Operator): return {"FINISHED"} +class WTD_OT_scene_request(bpy.types.Operator): + bl_idname = "wtd_scene.request" + bl_label = "Request" + bl_description = ( + "Ask Libreflow to download the latest publish on this site (from the exchange site, " + "or from the site that published it). Refresh once it is downloaded" + ) + + file_oid: StringProperty(description="Only the latest publish of this file", options={"SKIP_SAVE"}) + + @classmethod + def poll(cls, context): + return connected() + + def execute(self, context): + file_oids = [self.file_oid] if self.file_oid else missing_files(context.scene) + revisions = {} + for file_oid in file_oids: + revision = to_request(file_data(file_oid)) + if revision is not None: + revisions[revision["oid"]] = file_oid + if not revisions: + self.report({"INFO"}, "Nothing to request") + return {"CANCELLED"} + request_revisions(revisions) + return {"FINISHED"} + + def _revision_items(self, context): return _REVISION_ITEMS @@ -524,7 +614,12 @@ class WTD_OT_scene_pick_revision(bpy.types.Operator): ( r["name"], f"{r['name']} {time.strftime('%Y-%m-%d', time.localtime(r['date'])) if r.get('date') else ''}" - f" {r.get('user') or ''}" + ("" if r.get("available") else " (not on this site)"), + f" {r.get('user') or ''}" + + ( + "" + if r.get("available") + else " (requested)" if r.get("sync_status") == "Requested" else " (not on this site: request)" + ), r.get("comment") or "", "CHECKMARK" if r["name"] == current else "NONE", index, @@ -542,9 +637,14 @@ class WTD_OT_scene_pick_revision(bpy.types.Operator): library = bpy.data.libraries.get(self.library) if library is None: return {"CANCELLED"} - revision = next(r for r in STATE.revisions[library["wtd_file_oid"]] if r["name"] == self.revision) + file_oid = library["wtd_file_oid"] + revision = next(r for r in STATE.revisions[file_oid] if r["name"] == self.revision) if not revision.get("available"): - self.report({"WARNING"}, f"{revision['name']} is not available on this site") + if revision.get("sync_status") == "Requested": + self.report({"WARNING"}, f"{revision['name']} is requested, waiting for the download") + else: + request_revisions({revision["oid"]: file_oid}) + self.report({"INFO"}, f"{revision['name']} is not on this site: requested, pick it again once downloaded") return {"CANCELLED"} updated, messages = _update_libraries([self.library], target=revision) if messages: @@ -844,6 +944,7 @@ classes = ( WTD_OT_scene_refresh, WTD_OT_scene_load, WTD_OT_scene_update, + WTD_OT_scene_request, WTD_OT_scene_pick_revision, WTD_OT_scene_remove, WTD_OT_scene_select_instance, diff --git a/ui/panel.py b/ui/panel.py index 3fc8876..d7a0d07 100644 --- a/ui/panel.py +++ b/ui/panel.py @@ -384,6 +384,8 @@ class _SceneCastingPanel: row = layout.row(align=True) row.scale_y = 1.3 row.operator(sm.WTD_OT_scene_load.bl_idname, text="Load Missing", icon="IMPORT") + if sm.missing_files(context.scene): + row.operator(sm.WTD_OT_scene_request.bl_idname, text="Request Missing", icon="URL") row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="FILE_REFRESH") casting = sm.STATE.casting.get(oid) if casting is None: @@ -400,16 +402,23 @@ class _SceneCastingPanel: have = len(found.get(library.name, [])) if library else 0 want = asset.get("nb_occurences") or 1 error = asset.get("error") - icon = "ERROR" if error else "CHECKMARK" if have >= want else "IMPORT" + file = sm.file_data(asset["file"]["file_oid"], asset["file"]) if asset.get("file") else None + latest = (file or {}).get("latest") or {} + requested = bool(error) and latest.get("sync_status") == "Requested" + if requested: + error = f"{latest['name']} requested, waiting for the download" + icon = "SORTTIME" if requested else "ERROR" if error else "CHECKMARK" if have >= want else "IMPORT" row = col.row(align=True) - row.alert = bool(error) + row.alert = bool(error) and not requested row.label(text=f"{asset['name']} ({asset.get('asset_type', '')})", icon=icon) row.label(text=f"{have}/{want}") if not error and have < want: row.operator(sm.WTD_OT_scene_load.bl_idname, text="", icon="IMPORT").asset_oid = asset["oid"] + if sm.to_request(file): + row.operator(sm.WTD_OT_scene_request.bl_idname, text="", icon="URL").file_oid = file["file_oid"] if error: sub = col.row() - sub.alert = True + sub.alert = not requested _wrapped_label(sub, context, error, icon="BLANK1") cast = {asset["oid"] for asset in casting["assets"]} @@ -455,6 +464,8 @@ class _SceneLoadedPanel: version.label(text=library.get("wtd_revision", "?")) if state == "UPDATE": row.operator(sm.WTD_OT_scene_update.bl_idname, text="", icon="FILE_REFRESH").library = library.name + elif state == "PENDING" and sm.to_request(sm.file_data(library["wtd_file_oid"])): + row.operator(sm.WTD_OT_scene_request.bl_idname, text="", icon="URL").file_oid = library["wtd_file_oid"] row.operator(sm.WTD_OT_scene_pick_revision.bl_idname, text="", icon="COLLAPSEMENU").library = library.name row.operator(sm.WTD_OT_scene_remove.bl_idname, text="", icon="X").library = library.name if outdated: