Show soundtrack covers and edit alternative names (#49)
Covers: statistics cards show the soundtrack's cover on the left, and the
statistics popup shows it large. Until it arrives, and without a cover, the
soundtrack's initials stand in. Covers load only for cards on or near the
screen, three at a time, and are cached in covers/ next to the settings
(CoverCache), downloaded again only when cover_modified changes. The index and
file handling are shared with CharacterCache in a new FileCache.
Alternative names: the popup shows them and has an editor (add, remove, save)
that sends PUT /soundtracks/{uuid}/alternative-names. The statistics search and
the Search popup find soundtracks by them; Search shows the matching name after
the real one. Search now loads /soundtracks, falling back to the plain list.
The API types are regenerated from the server's spec, with a SoundtrackCover
theme style in every theme.
Closes #49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a735c515bf
commit
ef6ac8f1fd
37 files changed
+8029
-1763
No files matched your search
@@ -0,0 +1,187 @@
|
||||
extends MainWindowTestSuite
|
||||
## Soundtrack covers on the statistics cards and in the popup, loaded for the cards on
|
||||
## screen, and editing a soundtrack's alternative names, which search then finds it by.
|
||||
|
||||
const STATISTICS: Array[Dictionary] = [
|
||||
{"soundtrack_uuid": "st-1", "soundtrack_name": "Chrono Trigger", "soundtrack_played": 5,
|
||||
"cover_modified": 100, "alternative_names": [], "songs": [
|
||||
{"song_name": "Corridors of Time", "times_played": 1},
|
||||
]},
|
||||
{"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0,
|
||||
"songs": []},
|
||||
]
|
||||
|
||||
var _view: StatisticsView
|
||||
var _popup: SoundtrackStatsPopup
|
||||
var _png: PackedByteArray
|
||||
|
||||
|
||||
func _before_window_starts() -> void:
|
||||
_server.respond_json("/statistics/soundtracks", STATISTICS)
|
||||
var image: Image = Image.create(6, 8, false, Image.FORMAT_RGB8)
|
||||
_png = image.save_png_to_buffer()
|
||||
_server.respond_bytes("/soundtracks/st-1/cover", _png)
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
# Covers are kept next to the settings, which the tests keep in a folder of their own
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("covers_%d" % randi()))
|
||||
await super()
|
||||
_view = _window.get_node("StatisticsView")
|
||||
_popup = _window.get_node("SoundtrackStatsPopup")
|
||||
|
||||
|
||||
func _open_statistics() -> void:
|
||||
_window.show_tab(&"statistics")
|
||||
await _runner.simulate_frames(3)
|
||||
|
||||
|
||||
func _card(soundtrack_name: String) -> SoundtrackCard:
|
||||
for card: SoundtrackCard in _view.cards:
|
||||
if card.soundtrack.soundtrack_name == soundtrack_name:
|
||||
return card
|
||||
return null
|
||||
|
||||
|
||||
func test_card_shows_its_cover() -> void:
|
||||
await _open_statistics()
|
||||
|
||||
var card: SoundtrackCard = _card("Chrono Trigger")
|
||||
assert_object(card.cover_picture.texture).is_not_null()
|
||||
assert_bool(card.cover_placeholder.visible).is_false()
|
||||
|
||||
|
||||
func test_card_without_a_cover_shows_initials_and_asks_nothing() -> void:
|
||||
await _open_statistics()
|
||||
|
||||
var card: SoundtrackCard = _card("Terranigma")
|
||||
assert_object(card.cover_picture.texture).is_null()
|
||||
assert_bool(card.cover_placeholder.visible).is_true()
|
||||
assert_str(card.cover_placeholder.text).is_equal("T")
|
||||
assert_int(_server.count("/soundtracks/st-2/cover")).is_equal(0)
|
||||
|
||||
|
||||
func test_cover_missing_on_the_server_keeps_the_initials() -> void:
|
||||
_server.respond_bytes("/soundtracks/st-1/cover", PackedByteArray(), 404)
|
||||
|
||||
await _open_statistics()
|
||||
|
||||
assert_bool(_card("Chrono Trigger").cover_placeholder.visible).is_true()
|
||||
assert_str(_card("Chrono Trigger").cover_placeholder.text).is_equal("CT")
|
||||
|
||||
|
||||
func test_covers_load_for_cards_on_screen_as_they_scroll_in() -> void:
|
||||
var many: Array[Dictionary] = []
|
||||
for i: int in 40:
|
||||
many.append({"soundtrack_uuid": "many-%02d" % i, "soundtrack_name": "Game %02d" % i,
|
||||
"soundtrack_played": 1, "cover_modified": 100, "songs": []})
|
||||
_server.respond_bytes("/soundtracks/many-%02d/cover" % i, _png)
|
||||
_server.respond_json("/statistics/soundtracks", many)
|
||||
|
||||
await _open_statistics()
|
||||
|
||||
var asked: Callable = func() -> int:
|
||||
return _server.requested_paths.filter(
|
||||
func(path: String) -> bool: return path.begins_with("/soundtracks/many-")).size()
|
||||
var on_screen: int = asked.call()
|
||||
assert_int(on_screen).is_greater(0)
|
||||
assert_int(on_screen).is_less(40)
|
||||
|
||||
var scroll_bar: VScrollBar = _view.card_scroll.get_v_scroll_bar()
|
||||
scroll_bar.value = scroll_bar.max_value
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
assert_int(asked.call()).is_greater(on_screen)
|
||||
|
||||
|
||||
func test_popup_shows_the_cover_large() -> void:
|
||||
await _open_statistics()
|
||||
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
|
||||
assert_object(_popup.cover_picture.texture).is_not_null()
|
||||
assert_bool(_popup.cover_placeholder.visible).is_false()
|
||||
assert_str(_popup.names_label.text).is_equal("No other names yet")
|
||||
|
||||
|
||||
func test_editing_names_saves_them_and_search_finds_the_new_name() -> void:
|
||||
_server.respond_json("/soundtracks/st-1/alternative-names", {"uuid": "st-1",
|
||||
"name": "Chrono Trigger", "alternative_names": ["CT", "クロノ・トリガー"]})
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
|
||||
_popup.edit_names()
|
||||
assert_bool(_popup.names_editor.visible).is_true()
|
||||
_popup.new_name_field.text = "CT"
|
||||
_popup.add_name()
|
||||
_popup.new_name_field.text = "クロノ・トリガー"
|
||||
_popup.save_names()
|
||||
|
||||
var index: int = _server.requested_paths.find("/soundtracks/st-1/alternative-names")
|
||||
assert_int(index).is_not_equal(-1)
|
||||
assert_int(_server.requested_methods[index]).is_equal(HTTPClient.METHOD_PUT)
|
||||
var body: Dictionary = JSON.parse_string(_server.requested_bodies[index])
|
||||
assert_array(body.names).contains_exactly(["CT", "クロノ・トリガー"])
|
||||
assert_bool(_popup.names_editor.visible).is_false()
|
||||
assert_str(_popup.names_label.text).is_equal("CT, クロノ・トリガー")
|
||||
|
||||
_view.search_field.text = "クロノ"
|
||||
_view.apply_filter()
|
||||
var shown: Array = _view.cards.filter(func(card: SoundtrackCard) -> bool: return card.visible)
|
||||
assert_array(shown.map(func(card: SoundtrackCard) -> String:
|
||||
return card.soundtrack.soundtrack_name)).contains_exactly(["Chrono Trigger"])
|
||||
|
||||
|
||||
func test_editor_skips_empty_repeated_and_own_names() -> void:
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
_popup.edit_names()
|
||||
|
||||
for name: String in [" ", "CT", "ct", "Chrono Trigger"]:
|
||||
_popup.new_name_field.text = name
|
||||
_popup.add_name()
|
||||
|
||||
assert_int(_popup.editor_names.get_child_count()).is_equal(1)
|
||||
|
||||
|
||||
func test_removing_a_name_in_the_editor() -> void:
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
_popup.edit_names()
|
||||
_popup.new_name_field.text = "CT"
|
||||
_popup.add_name()
|
||||
|
||||
_popup.remove_name("CT")
|
||||
|
||||
assert_int(_popup.editor_names.get_child_count()).is_equal(0)
|
||||
|
||||
|
||||
func test_failed_save_keeps_the_editor_open() -> void:
|
||||
_server.respond_json("/soundtracks/st-1/alternative-names", {}, 500)
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
_popup.edit_names()
|
||||
_popup.new_name_field.text = "CT"
|
||||
|
||||
_popup.save_names()
|
||||
|
||||
assert_bool(_popup.names_editor.visible).is_true()
|
||||
assert_bool(_popup.editor_status.visible).is_true()
|
||||
assert_bool(_popup.save_names_button.disabled).is_false()
|
||||
|
||||
|
||||
func test_search_popup_finds_soundtracks_by_alternative_names() -> void:
|
||||
_server.respond_json("/soundtracks", [
|
||||
{"uuid": "st-1", "name": "Chrono Trigger", "alternative_names": ["CT"]},
|
||||
{"uuid": "st-2", "name": "Terranigma", "alternative_names": []},
|
||||
])
|
||||
var search: SearchPopup = _window.get_node("SearchPopup")
|
||||
search.get_list_of_soundtracks()
|
||||
search.open()
|
||||
|
||||
search.search_field.text = "ct"
|
||||
search.search()
|
||||
|
||||
assert_array(search.shown_names()).contains(["Chrono Trigger"])
|
||||
var row: PanelContainer = search.search_results.get_child(0)
|
||||
assert_str((row.get_child(0) as RichTextLabel).get_parsed_text()).contains("CT")
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3oc3e1fsttoh
|
||||
@@ -50,6 +50,7 @@ const STYLES: Dictionary = {
|
||||
"ProgressSlider": "HSlider",
|
||||
"VolumeBar": "HSlider",
|
||||
"StatisticsCard": "PanelContainer",
|
||||
"SoundtrackCover": "Panel",
|
||||
"StatisticsCardHover": "PanelContainer",
|
||||
"StatisticsGrid": "GridContainer",
|
||||
"TabButton": "Button",
|
||||
|
||||
Reference in new issue
Block a user