dock thumbnails: preview pane, Bearer auth fix, headless panel test

This commit is contained in:
BillyOutlast committed 2026-10-25 19:15:43 -09:00
1 parent 89b3b4eed5
commit 745bdc9e73
3 files changed
+150 -15

No files matched your search

+60 -15
View File
@@ -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:
@@ -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"
+85
View File
@@ -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