Change soundtrack covers from the app and show the cover with the answer
Cover menu in the soundtrack popup ("Change cover"):
- Choose picture uploads a JPEG, PNG or WebP picture (up to 10 MB).
- Pick by ID uses a game's cover on SteamGridDB or an anime's on AniList.
- Remove cover deletes it, so the server searches for a new one.
- The server's reason is shown when it refuses a change.
After a change the new cover time is fetched, and the popup, the
statistics card and the quiz show the new cover.
Quiz: once the answer is shown, the soundtrack's cover shows to the left of
the titles. Cover and titles are centered together, left and right and up
and down, and each title keeps to two lines. Cover times come from the
soundtrack list, and songs keep their soundtrack's UUID, also in saves.
Requests can now send any body with its content type, and every 2xx answer
counts as success. Cover frames are 2:3 like the server's covers. Small
copies of real covers are used by the tests and screenshots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef6ac8f1fd
commit
06421bb63e
33 files changed
+3966
-920
No files matched your search
@@ -206,7 +206,7 @@ func test_every_button_bounces() -> void:
|
||||
|
||||
func test_helpers_do_nothing_with_reduced_animations() -> void:
|
||||
Settings.reduce_animations = true
|
||||
var label: Label = _quiz.get_node("NowPlaying/SongLabel")
|
||||
var label: Label = _quiz.get_node("NowPlaying/Titles/SongLabel")
|
||||
|
||||
Anim.pop(label)
|
||||
Anim.fade_in(label)
|
||||
|
||||
@@ -55,8 +55,8 @@ func _play_a_little() -> Array[PlayerObject]:
|
||||
|
||||
func _now_playing() -> Array[String]:
|
||||
var now_playing: Node = _quiz.get_node("NowPlaying")
|
||||
return [(now_playing.get_node("SoundtrackLabel") as Label).text,
|
||||
(now_playing.get_node("SongLabel") as Label).text]
|
||||
return [(now_playing.get_node("Titles/SoundtrackLabel") as Label).text,
|
||||
(now_playing.get_node("Titles/SongLabel") as Label).text]
|
||||
|
||||
|
||||
# Saving
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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.
|
||||
## Soundtrack covers on the statistics cards, in the popup and next to the answer in the
|
||||
## quiz, loaded for the cards on screen; changing a cover from the popup; 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,
|
||||
@@ -21,6 +22,13 @@ func _before_window_starts() -> void:
|
||||
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)
|
||||
# The soundtrack list tells the quiz which soundtracks have covers
|
||||
_server.respond_json("/soundtracks", [
|
||||
{"uuid": "st-1", "name": "Chrono Trigger", "cover_modified": 100},
|
||||
{"uuid": "st-2", "name": "Terranigma"},
|
||||
])
|
||||
_next_song_is({"SongNo": 0, "Soundtrack": "Chrono Trigger", "SoundtrackUuid": "st-1",
|
||||
"Song": "Corridors of Time"})
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
@@ -185,3 +193,200 @@ func test_search_popup_finds_soundtracks_by_alternative_names() -> void:
|
||||
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")
|
||||
|
||||
|
||||
func _open_cover_menu() -> void:
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
|
||||
_popup.change_cover()
|
||||
|
||||
|
||||
# The server's answer for the soundtrack once its cover changed
|
||||
func _cover_time_becomes(modified: int) -> void:
|
||||
_server.respond_json("/soundtracks/st-1", {"uuid": "st-1", "name": "Chrono Trigger",
|
||||
"cover_modified": modified, "songs": []})
|
||||
|
||||
|
||||
func _picture_file(bytes: PackedByteArray) -> String:
|
||||
var path: String = create_temp_dir("pictures_%d" % randi()).path_join("cover.jpg")
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
return path
|
||||
|
||||
|
||||
func _downloads(path: String) -> int:
|
||||
var count: int = 0
|
||||
for i: int in _server.requested_paths.size():
|
||||
if _server.requested_paths[i] == path \
|
||||
&& _server.requested_methods[i] == HTTPClient.METHOD_GET:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _now_playing() -> NowPlaying:
|
||||
return _quiz.get_node("NowPlaying")
|
||||
|
||||
|
||||
func test_cover_menu_opens_in_place_of_the_songs() -> void:
|
||||
await _open_cover_menu()
|
||||
|
||||
assert_bool(_popup.cover_editor.visible).is_true()
|
||||
assert_bool(_popup.song_scroll.visible).is_false()
|
||||
assert_bool(_popup.edit_names_button.disabled).is_true()
|
||||
assert_bool(_popup.remove_cover_button.disabled).is_false()
|
||||
|
||||
_popup.stop_editing()
|
||||
|
||||
assert_bool(_popup.cover_editor.visible).is_false()
|
||||
assert_bool(_popup.song_scroll.visible).is_true()
|
||||
|
||||
|
||||
func test_soundtrack_without_a_cover_has_nothing_to_remove() -> void:
|
||||
await _open_statistics()
|
||||
_popup.show_soundtrack(_card("Terranigma").soundtrack)
|
||||
|
||||
_popup.change_cover()
|
||||
|
||||
assert_bool(_popup.remove_cover_button.disabled).is_true()
|
||||
|
||||
|
||||
func test_uploaded_picture_becomes_the_cover() -> void:
|
||||
var bytes: PackedByteArray = FileAccess.get_file_as_bytes(
|
||||
"res://tests/data/covers/cover_0.jpg")
|
||||
_cover_time_becomes(200)
|
||||
await _open_cover_menu()
|
||||
|
||||
_popup.upload_picture(_picture_file(bytes))
|
||||
|
||||
var index: int = _server.requested_methods.find(HTTPClient.METHOD_PUT)
|
||||
assert_str(_server.requested_paths[index]).is_equal("/soundtracks/st-1/cover")
|
||||
assert_array(_server.requested_raw_bodies[index]).is_equal(bytes)
|
||||
assert_array(_server.requested_headers[index]).contains(["Content-Type: image/jpeg"])
|
||||
# The new cover is downloaded again for the popup and the card
|
||||
assert_int(_card("Chrono Trigger").soundtrack.cover_modified).is_equal(200)
|
||||
await _runner.simulate_frames(2)
|
||||
# Downloaded once more, for the popup and the card together
|
||||
assert_int(_downloads("/soundtracks/st-1/cover")).is_equal(2)
|
||||
assert_object(_card("Chrono Trigger").cover_picture.texture).is_not_null()
|
||||
assert_str(_popup.cover_status.text).is_equal("The new cover is saved.")
|
||||
|
||||
|
||||
func test_file_that_is_not_a_picture_is_not_uploaded() -> void:
|
||||
await _open_cover_menu()
|
||||
|
||||
_popup.upload_picture(_picture_file("not a picture".to_utf8_buffer()))
|
||||
|
||||
assert_int(_server.requested_methods.count(HTTPClient.METHOD_PUT)).is_equal(0)
|
||||
assert_str(_popup.cover_status.text).contains("JPEG, PNG and WebP")
|
||||
|
||||
|
||||
func test_cover_picked_by_id() -> void:
|
||||
_server.respond_bytes("/soundtracks/st-1/cover/source", PackedByteArray(), 204)
|
||||
_cover_time_becomes(300)
|
||||
await _open_cover_menu()
|
||||
|
||||
_popup.cover_source_button.select(1)
|
||||
_popup.cover_id_field.text = " 21 "
|
||||
_popup.use_cover_source()
|
||||
|
||||
var index: int = _server.requested_paths.find("/soundtracks/st-1/cover/source")
|
||||
assert_int(_server.requested_methods[index]).is_equal(HTTPClient.METHOD_PUT)
|
||||
var body: Dictionary = JSON.parse_string(_server.requested_bodies[index])
|
||||
assert_str(body.source).is_equal("anilist")
|
||||
assert_str(body.id).is_equal("21")
|
||||
assert_int(_popup.soundtrack.cover_modified).is_equal(300)
|
||||
|
||||
|
||||
func test_no_id_asks_nothing() -> void:
|
||||
await _open_cover_menu()
|
||||
|
||||
_popup.use_cover_source()
|
||||
|
||||
assert_int(_server.count("/soundtracks/st-1/cover/source")).is_equal(0)
|
||||
|
||||
|
||||
func test_removed_cover_shows_the_initials() -> void:
|
||||
_cover_time_becomes(0)
|
||||
await _open_cover_menu()
|
||||
|
||||
_popup.remove_cover()
|
||||
|
||||
var index: int = _server.requested_methods.find(HTTPClient.METHOD_DELETE)
|
||||
assert_str(_server.requested_paths[index]).is_equal("/soundtracks/st-1/cover")
|
||||
assert_bool(_popup.cover_placeholder.visible).is_true()
|
||||
assert_bool(_card("Chrono Trigger").cover_placeholder.visible).is_true()
|
||||
assert_bool(FileAccess.file_exists(CoverCache.folder().path_join("st-1"))).is_false()
|
||||
assert_bool(_popup.remove_cover_button.disabled).is_true()
|
||||
|
||||
|
||||
func test_refused_change_shows_the_servers_reason() -> void:
|
||||
_server.respond_json("/soundtracks/st-1/cover/source",
|
||||
{"error": "covers are not enabled on this server"}, 503)
|
||||
await _open_cover_menu()
|
||||
_popup.cover_id_field.text = "21"
|
||||
|
||||
_popup.use_cover_source()
|
||||
|
||||
assert_str(_popup.cover_status.text).is_equal(
|
||||
"Could not change the cover: covers are not enabled on this server.")
|
||||
assert_bool(_popup.use_source_button.disabled).is_false()
|
||||
|
||||
|
||||
func test_answer_shows_the_soundtracks_cover() -> void:
|
||||
await _play_first_song()
|
||||
|
||||
assert_bool(_now_playing().has_cover()).is_false()
|
||||
|
||||
_press("SongControls/ShowAnswerButton")
|
||||
|
||||
assert_bool(_now_playing().has_cover()).is_true()
|
||||
await _runner.simulate_frames(1)
|
||||
# The cover sits just left of the titles, and the two are centered together
|
||||
var area: Rect2 = _now_playing().get_global_rect()
|
||||
var cover: Rect2 = _now_playing().cover_frame.get_global_rect()
|
||||
var titles: Rect2 = _now_playing().titles.get_global_rect()
|
||||
assert_float(titles.position.x - cover.end.x).is_equal(32.0)
|
||||
assert_float(absf((cover.position.x - area.position.x) - (area.end.x - titles.end.x))) \
|
||||
.is_less_equal(1.0)
|
||||
# Up and down, the cover is centered between the top bar and the song list, and the
|
||||
# titles on the cover
|
||||
assert_float(absf((cover.position.y - area.position.y) - (area.end.y - cover.end.y))) \
|
||||
.is_less_equal(1.0)
|
||||
var text_middle: float = (_now_playing().soundtrack_label.get_global_rect().position.y
|
||||
+ _now_playing().song_label.get_global_rect().end.y) / 2.0
|
||||
assert_float(absf(text_middle - cover.get_center().y)).is_less_equal(1.0)
|
||||
|
||||
|
||||
func test_answer_without_a_cover_shows_only_the_titles() -> void:
|
||||
_view.covers.set_cover_times({"st-1": 0})
|
||||
await _play_first_song()
|
||||
|
||||
_press("SongControls/ShowAnswerButton")
|
||||
|
||||
assert_bool(_now_playing().has_cover()).is_false()
|
||||
assert_int(_server.count("/soundtracks/st-1/cover")).is_equal(0)
|
||||
|
||||
|
||||
func test_next_song_hides_the_cover_until_its_answer() -> void:
|
||||
await _play_first_song()
|
||||
_press("SongControls/ShowAnswerButton")
|
||||
|
||||
_press("SongControls/NextButton")
|
||||
# The playing song is stopped first, which takes half a second
|
||||
await await_millis(700)
|
||||
|
||||
assert_int(Playlist.currently_playing_song).is_equal(1)
|
||||
assert_bool(_now_playing().has_cover()).is_false()
|
||||
|
||||
|
||||
func test_changed_cover_shows_in_the_quiz_too() -> void:
|
||||
await _play_first_song()
|
||||
_press("SongControls/ShowAnswerButton")
|
||||
_cover_time_becomes(0)
|
||||
_popup.show_soundtrack(SoundtrackWithSongs.from_dict(STATISTICS[0]))
|
||||
_popup.change_cover()
|
||||
|
||||
_popup.remove_cover()
|
||||
|
||||
assert_bool(_now_playing().has_cover()).is_false()
|
||||
@@ -33,7 +33,7 @@ func test_next_plays_preloaded_song_and_fetches_another() -> void:
|
||||
|
||||
|
||||
func test_title_is_hidden_until_answer_is_shown() -> void:
|
||||
var soundtrack_label: Label = _quiz.get_node("NowPlaying/SoundtrackLabel")
|
||||
var soundtrack_label: Label = _quiz.get_node("NowPlaying/Titles/SoundtrackLabel")
|
||||
_press("SongControls/NextButton")
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
@@ -224,7 +224,7 @@ func test_show_answer_before_first_song_does_not_reveal_next_song() -> void:
|
||||
|
||||
await _play_first_song()
|
||||
|
||||
assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \
|
||||
assert_str((_quiz.get_node("NowPlaying/Titles/SoundtrackLabel") as Label).text) \
|
||||
.is_equal("????????")
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ func before_test() -> void:
|
||||
|
||||
|
||||
func _label(label_name: String) -> Label:
|
||||
return _now_playing.get_node(label_name)
|
||||
return _now_playing.get_node("Titles/" + label_name)
|
||||
|
||||
|
||||
func test_starts_empty() -> void:
|
||||
@@ -32,3 +32,16 @@ func test_soundtrack_is_above_song() -> void:
|
||||
|
||||
assert_float(_label("SoundtrackLabel").get_global_rect().end.y) \
|
||||
.is_less_equal(_label("SongLabel").get_global_rect().position.y)
|
||||
|
||||
|
||||
func test_long_titles_keep_to_two_lines_each() -> void:
|
||||
_now_playing.size = Vector2(1088, 328)
|
||||
_now_playing.show_song("Final Fantasy Dissidia 012 Duodecim (Sista Fantasin "
|
||||
+ "Meningsskiljaktigheter 012 Tolv) Final Mix Special Edition",
|
||||
"Cosmos (Arrangement) - Main Theme of the Warriors of Light, Long Version")
|
||||
await _runner.simulate_frames(2)
|
||||
|
||||
for label_name: String in ["SoundtrackLabel", "SongLabel"]:
|
||||
assert_int(_label(label_name).get_visible_line_count()).is_less_equal(2)
|
||||
assert_float(_label("SongLabel").get_global_rect().end.y) \
|
||||
.is_less_equal(_now_playing.get_global_rect().end.y)
|
||||
@@ -54,8 +54,8 @@ func _next() -> void:
|
||||
|
||||
func _now_playing() -> Array[String]:
|
||||
var now_playing: Node = _quiz.get_node("NowPlaying")
|
||||
return [(now_playing.get_node("SoundtrackLabel") as Label).text,
|
||||
(now_playing.get_node("SongLabel") as Label).text]
|
||||
return [(now_playing.get_node("Titles/SoundtrackLabel") as Label).text,
|
||||
(now_playing.get_node("Titles/SongLabel") as Label).text]
|
||||
|
||||
|
||||
func test_picking_a_folder_makes_no_server_requests() -> void:
|
||||
|
||||
@@ -121,12 +121,12 @@ func test_clicking_quiz_buttons_plays_and_shows_answer() -> void:
|
||||
await _click_at(_center(_quiz.get_node("SongControls/NextButton")))
|
||||
await _runner.simulate_frames(2)
|
||||
assert_int(Playlist.currently_playing_song).is_equal(0)
|
||||
assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \
|
||||
assert_str((_quiz.get_node("NowPlaying/Titles/SoundtrackLabel") as Label).text) \
|
||||
.is_not_equal("Chrono Trigger")
|
||||
|
||||
await _click_at(_center(_quiz.get_node("SongControls/ShowAnswerButton")))
|
||||
|
||||
assert_str((_quiz.get_node("NowPlaying/SoundtrackLabel") as Label).text) \
|
||||
assert_str((_quiz.get_node("NowPlaying/Titles/SoundtrackLabel") as Label).text) \
|
||||
.is_equal("Chrono Trigger")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user