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
+38
-3
@@ -2,22 +2,57 @@
|
||||
|
||||
[ext_resource type="Script" path="res://quiz/now_playing.gd" id="part_script"]
|
||||
|
||||
[node name="NowPlaying" type="VBoxContainer" unique_id=42468498]
|
||||
[node name="NowPlaying" type="HBoxContainer" unique_id=42468498]
|
||||
layout_mode = 3
|
||||
offset_right = 1088.0
|
||||
offset_bottom = 320.0
|
||||
theme_override_constants/separation = 32
|
||||
alignment = 1
|
||||
script = ExtResource("part_script")
|
||||
|
||||
[node name="SoundtrackLabel" type="Label" parent="." unique_id=173147793]
|
||||
[node name="CoverFrame" type="Panel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
clip_children = 2
|
||||
custom_minimum_size = Vector2(180, 270)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"SoundtrackCover"
|
||||
|
||||
[node name="CoverPicture" type="TextureRect" parent="CoverFrame"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 6
|
||||
|
||||
[node name="Titles" type="VBoxContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="SoundtrackLabel" type="Label" parent="Titles" unique_id=173147793]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1, 1)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"TitleLarge"
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
text_overrun_behavior = 3
|
||||
max_lines_visible = 2
|
||||
|
||||
[node name="SongLabel" type="Label" parent="." unique_id=1687490027]
|
||||
[node name="SongLabel" type="Label" parent="Titles" unique_id=1687490027]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1, 1)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"TitleMedium"
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
text_overrun_behavior = 3
|
||||
max_lines_visible = 2
|
||||
+1
-1
@@ -41,7 +41,7 @@ offset_bottom = 512.0
|
||||
[node name="NowPlaying" parent="." unique_id=42468498 instance=ExtResource("scene_NowPlaying")]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 64.0
|
||||
offset_top = 56.0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 384.0
|
||||
|
||||
|
||||
+97
-8
@@ -1,16 +1,105 @@
|
||||
class_name NowPlaying
|
||||
extends VBoxContainer
|
||||
extends HBoxContainer
|
||||
## The soundtrack and song that are playing, in large text at the top of the quiz. They
|
||||
## fade in when they change.
|
||||
## fade in when they change. Once the answer is shown, the soundtrack's cover shows on
|
||||
## their left, when it has one, and the cover and titles are centered together. Both are
|
||||
## centered up and down too, so the titles only move sideways when a cover comes. Each
|
||||
## title has at most two lines.
|
||||
|
||||
## The covers on disk. No cover is shown without it.
|
||||
var covers: CoverCache:
|
||||
set(value):
|
||||
covers = value
|
||||
if covers != null:
|
||||
covers.cover_times_changed.connect(_load_cover)
|
||||
_load_cover()
|
||||
|
||||
# The soundtrack whose cover is shown or on its way, and the cover time asked for
|
||||
var _cover_uuid: String = ""
|
||||
var _cover_time: int = 0
|
||||
|
||||
@onready var soundtrack_label: Label = %SoundtrackLabel
|
||||
@onready var song_label: Label = %SongLabel
|
||||
@onready var cover_frame: Panel = %CoverFrame
|
||||
@onready var cover_picture: TextureRect = %CoverPicture
|
||||
@onready var titles: VBoxContainer = %Titles
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resized.connect(_fit_titles)
|
||||
_fit_titles()
|
||||
|
||||
|
||||
## Shows [param soundtrack_title] and [param song_title]. The playlist hides them as
|
||||
## question marks until the answer is shown.
|
||||
func show_song(soundtrack_title: String, song_title: String) -> void:
|
||||
var changed: bool = $SoundtrackLabel.text != soundtrack_title \
|
||||
|| $SongLabel.text != song_title
|
||||
$SoundtrackLabel.text = soundtrack_title
|
||||
$SongLabel.text = song_title
|
||||
## question marks until the answer is shown, and only then gives the
|
||||
## [param soundtrack_uuid] whose cover is shown.
|
||||
func show_song(soundtrack_title: String, song_title: String,
|
||||
soundtrack_uuid: String = "") -> void:
|
||||
var changed: bool = soundtrack_label.text != soundtrack_title \
|
||||
|| song_label.text != song_title
|
||||
soundtrack_label.text = soundtrack_title
|
||||
song_label.text = song_title
|
||||
_fit_titles()
|
||||
if soundtrack_uuid != _cover_uuid:
|
||||
_cover_uuid = soundtrack_uuid
|
||||
_cover_time = 0
|
||||
_show_cover(null)
|
||||
_load_cover()
|
||||
# A new song, or the answer being shown, fades in
|
||||
if changed:
|
||||
Anim.fade_in(self, Anim.SLOW)
|
||||
|
||||
|
||||
## Whether a cover is shown.
|
||||
func has_cover() -> bool:
|
||||
return cover_frame.visible
|
||||
|
||||
|
||||
# Asks for the cover, again when its time changed, e.g. once the soundtrack list loaded
|
||||
func _load_cover() -> void:
|
||||
if covers == null || _cover_uuid.is_empty() || !is_node_ready():
|
||||
return
|
||||
var time: int = covers.cover_time(_cover_uuid)
|
||||
if time == _cover_time:
|
||||
return
|
||||
_cover_time = time
|
||||
if time <= 0:
|
||||
_show_cover(null)
|
||||
return
|
||||
covers.fetch_cover(_cover_uuid, time, _show_cover_of.bind(_cover_uuid, time))
|
||||
|
||||
|
||||
# Covers arrive later; one for a song no longer shown is left out
|
||||
func _show_cover_of(cover: Image, uuid: String, time: int) -> void:
|
||||
if uuid == _cover_uuid && time == _cover_time:
|
||||
_show_cover(cover)
|
||||
|
||||
|
||||
func _show_cover(cover: Image) -> void:
|
||||
var was_shown: bool = cover_frame.visible
|
||||
cover_picture.texture = ImageTexture.create_from_image(cover) if cover != null else null
|
||||
cover_frame.visible = cover != null
|
||||
_fit_titles()
|
||||
if cover_frame.visible && !was_shown:
|
||||
Anim.fade_in(cover_frame, Anim.SLOW)
|
||||
|
||||
|
||||
# The titles take the width of the longer one, so they stay next to the cover, but no
|
||||
# more than there is room for; longer titles wrap. Beside a cover they line up with it
|
||||
# on the left.
|
||||
func _fit_titles() -> void:
|
||||
var room: float = size.x
|
||||
if cover_frame.visible:
|
||||
room -= cover_frame.custom_minimum_size.x + get_theme_constant(&"separation")
|
||||
var widest: float = maxf(_text_width(soundtrack_label), _text_width(song_label))
|
||||
titles.custom_minimum_size.x = clampf(ceilf(widest), 1.0, maxf(room, 1.0))
|
||||
var alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT if cover_frame.visible \
|
||||
else HORIZONTAL_ALIGNMENT_CENTER
|
||||
soundtrack_label.horizontal_alignment = alignment
|
||||
song_label.horizontal_alignment = alignment
|
||||
|
||||
|
||||
static func _text_width(label: Label) -> float:
|
||||
var font: Font = label.get_theme_font(&"font")
|
||||
return font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1,
|
||||
label.get_theme_font_size(&"font_size")).x
|
||||
+1
-1
@@ -171,7 +171,7 @@ func update_song_list() -> void:
|
||||
GameLog.debug("update_song_list")
|
||||
if Playlist.currently_playing_song >= 0:
|
||||
now_playing.show_song(Playlist.get_current_soundtrack_title(),
|
||||
Playlist.get_current_song_title())
|
||||
Playlist.get_current_song_title(), Playlist.get_current_soundtrack_uuid())
|
||||
music_list_panel.refresh()
|
||||
save_soon()
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ func load_song_list(saved: QuizSave = null) -> void:
|
||||
new_song_object.song_number = info.song_no
|
||||
new_song_object.soundtrack_title = info.soundtrack
|
||||
new_song_object.song_title = info.song
|
||||
new_song_object.soundtrack_uuid = info.soundtrack_uuid
|
||||
new_song_object.is_answered = true
|
||||
new_song_object.has_played = true
|
||||
Playlist.add_to_playlist(new_song_object)
|
||||
@@ -139,6 +140,7 @@ func _fetch_song_info(new_song_object: SongObject) -> void:
|
||||
new_song_object.song_number = info.song_no
|
||||
new_song_object.soundtrack_title = info.soundtrack
|
||||
new_song_object.song_title = info.song
|
||||
new_song_object.soundtrack_uuid = info.soundtrack_uuid
|
||||
GameLog.debug("new_song_object: ", new_song_object.to_string())
|
||||
Playlist.add_to_playlist(new_song_object)
|
||||
if Playlist.should_preload_more_songs():
|
||||
|
||||
Reference in new issue
Block a user