diff --git a/godot-plugin/addons/asset_library/asset_dock.gd b/godot-plugin/addons/asset_library/asset_dock.gd index b9c2087..33bffa9 100644 --- a/godot-plugin/addons/asset_library/asset_dock.gd +++ b/godot-plugin/addons/asset_library/asset_dock.gd @@ -3,11 +3,20 @@ extends VBoxContainer ## Dock panel: search the remote library, download into res://. const CONFIG_PATH := "user://asset_library.cfg" +const Client := preload("res://addons/asset_library/asset_client.gd") var _items: Array = [] var _http: HTTPRequest var _dl: HTTPRequest -var _dl_name := "" +var _thumb: HTTPRequest +var _dl_name: String = "" +var _url_field: LineEdit +var _token_field: LineEdit +var _query_field: LineEdit +var _go_button: Button +var _results: ItemList +var _preview: TextureRect +var _download_button: Button func _ready() -> void: @@ -17,38 +26,52 @@ func _ready() -> void: _dl = HTTPRequest.new() add_child(_dl) _dl.request_completed.connect(_on_blob_done) - %Go.pressed.connect(_on_search) - %Download.pressed.connect(_on_download) + _thumb = HTTPRequest.new() + add_child(_thumb) + _thumb.request_completed.connect(_on_thumb_done) + # Plain relative paths, not %-unique names: % resolution depends on + # scene-owner bookkeeping that is absent when the panel is instanced + # outside the editor (headless probes, runtime tools). + _url_field = get_node("URL") as LineEdit + _token_field = get_node("Token") as LineEdit + _query_field = get_node("SearchRow/Query") as LineEdit + _go_button = get_node("SearchRow/Go") as Button + _results = get_node("Results") as ItemList + _preview = get_node("Preview") as TextureRect + _download_button = get_node("Download") as Button + _go_button.pressed.connect(_on_search) + _results.item_selected.connect(_on_preview_index) + _download_button.pressed.connect(_on_download) _load_config() func _load_config() -> void: var cfg := ConfigFile.new() if cfg.load(CONFIG_PATH) == OK: - %URL.text = str(cfg.get_value("server", "url", "")) - %Token.text = str(cfg.get_value("server", "token", "")) + _url_field.text = str(cfg.get_value("server", "url", "")) + _token_field.text = str(cfg.get_value("server", "token", "")) func _save_config() -> void: var cfg := ConfigFile.new() - cfg.set_value("server", "url", %URL.text) - cfg.set_value("server", "token", %Token.text) + cfg.set_value("server", "url", _url_field.text) + cfg.set_value("server", "token", _token_field.text) cfg.save(CONFIG_PATH) func _api(path: String) -> String: - return %URL.text.strip_edges().trim_suffix("/") + path + return _url_field.text.strip_edges().trim_suffix("/") + path func _auth() -> PackedStringArray: return PackedStringArray( - ["Authorization: Bearer " + %Token.text]) + ["Authorization: Bearer " + _token_field.text]) func _on_search() -> void: _save_config() - var url := AssetLibraryClient.search_url( - _api("/api/v1/assets"), {"q": %Query.text}) + var url := Client.search_url( + _api("/api/v1/assets"), {"q": _query_field.text}) if _http.request(url, _auth()) != OK: push_error("asset-library: search request failed to start") @@ -58,14 +81,14 @@ func _on_list_done(result: int, _code: int, _headers: PackedStringArray, if result != HTTPRequest.RESULT_SUCCESS: push_error("asset-library: search failed") return - _items = AssetLibraryClient.parse_assets(body.get_string_from_utf8()) - %Results.clear() + _items = Client.parse_assets(body.get_string_from_utf8()) + _results.clear() for item: Variant in _items: - %Results.add_item(str((item as Dictionary).get("name", "?"))) + _results.add_item(str((item as Dictionary).get("name", "?"))) func _on_download() -> void: - var selected: PackedInt32Array = %Results.get_selected_items() + var selected: PackedInt32Array = _results.get_selected_items() if selected.is_empty(): return var item: Dictionary = _items[selected[0]] @@ -75,6 +98,28 @@ func _on_download() -> void: push_error("asset-library: download request failed to start") +func _on_preview_index(index: int) -> void: + if index < 0 or index >= _items.size(): + return + var item: Dictionary = _items[index] + var url := _api("/api/v1/assets/%d/thumbnail" % int(item.get("id", 0))) + if _thumb.request(url, _auth()) != OK: + push_error("asset-library: thumbnail request failed to start") + + +func _on_thumb_done(result: int, code: int, _headers: PackedStringArray, + body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS or code != 200: + _preview.texture = null + return + var image := Image.new() + if image.load_png_from_buffer(body) != OK: + push_warning("asset-library: thumbnail is not a PNG") + _preview.texture = null + return + _preview.texture = ImageTexture.create_from_image(image) + + func _on_blob_done(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if result != HTTPRequest.RESULT_SUCCESS or code != 200: diff --git a/godot-plugin/addons/asset_library/asset_dock.tscn b/godot-plugin/addons/asset_library/asset_dock.tscn index 63ff982..be73ad0 100644 --- a/godot-plugin/addons/asset_library/asset_dock.tscn +++ b/godot-plugin/addons/asset_library/asset_dock.tscn @@ -24,5 +24,10 @@ text = "Search" [node name="Results" type="ItemList" parent="." unique_name_in_owner=true] size_flags_vertical = 3 +[node name="Preview" type="TextureRect" parent="." unique_name_in_owner=true] +custom_minimum_size = Vector2(0, 128) +expand_mode = 1 +stretch_mode = 5 + [node name="Download" type="Button" parent="." unique_name_in_owner=true] text = "Download selected" diff --git a/tests/test_godot_plugin.py b/tests/test_godot_plugin.py index ddf7700..486cd0c 100644 --- a/tests/test_godot_plugin.py +++ b/tests/test_godot_plugin.py @@ -211,3 +211,88 @@ def test_full_loop_server_to_imported(tmp_path, godot_bin): got = list((tmp_path / "plugproj").rglob("*.ctex")) assert got, "no imported output produced" assert os.path.isfile(os.path.join(root, "hero.png.import")) + + +THUMB_PROBE_GD = """\ +extends SceneTree + +var _panel +var _phase := 0 +var _frames := 0 +var _base := "" +var _token := "" +var _want_id := 0 + + +func _initialize() -> void: +\tvar args := OS.get_cmdline_user_args() +\t_base = str(args[0]) +\t_token = str(args[1]) +\t_want_id = int(args[2]) +\t_panel = load("res://addons/asset_library/asset_dock.tscn").instantiate() +\troot.add_child(_panel) + + +func _process(_delta: float) -> bool: +\t_frames += 1 +\tif _frames > 900: +\t\tprint("E2E_THUMB:timeout") +\t\treturn true +\tif _phase == 0: +\t\tif _panel.get("_http") == null: +\t\t\treturn false +\t\t_panel.get_node("URL").text = _base +\t\t_panel.get_node("Token").text = _token +\t\t_panel.get_node("SearchRow/Query").text = "" +\t\t_panel._on_search() +\t\t_phase = 1 +\t\treturn false +\tif _phase == 1 and _panel._items.size() > 0: +\t\tvar first: Dictionary = _panel._items[0] +\t\tif int(first.get("id", -1)) == _want_id: +\t\t\t_panel._on_preview_index(0) +\t\t\t_phase = 2 +\t\telse: +\t\t\tprint("E2E_THUMB:wrong-id") +\t\t\treturn true +\telif _phase == 2: +\t\tvar tex: Texture2D = _panel.get_node("Preview").texture +\t\tif tex != null: +\t\t\tprint("E2E_THUMB:ok %dx%d" % [tex.get_width(), +\t\t\t\ttex.get_height()]) +\t\t\treturn true +\treturn false +""" + + +def test_dock_shows_server_thumbnail(tmp_path, godot_bin): + import base64 + import json + import subprocess + import sys + + from test_library_server import TOKEN, start_server + _, base = start_server(tmp_path / "srv") + png = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==") + source = tmp_path / "hero.png" + source.write_bytes(png) + root = make_plugin_project(tmp_path) + probe = os.path.join(root, "thumb_probe.gd") + with open(probe, "w", encoding="utf-8") as handle: + handle.write(THUMB_PROBE_GD) + # publish through the real CLI so importer metadata travels too + done = subprocess.run( + [sys.executable, "-m", "assetmgr.cli", "library-push", + str(source), "--server", base, "--token", TOKEN, + "--kind", "texture", "--importer", "texture", "--json"], + capture_output=True, text=True, timeout=120) + assert done.returncode == 0, done.stderr[-1000:] + asset_id = json.loads(done.stdout)["id"] + run = subprocess.run( + [godot_bin, "--headless", "--path", root, + "--script", probe, "--", + base, TOKEN, str(asset_id)], + capture_output=True, text=True, timeout=120) + assert run.returncode == 0, run.stderr[-2000:] + run.stdout[-1000:] + assert "E2E_THUMB:ok" in run.stdout, run.stdout