From a735c515bfead1b074e6a4f329f5ee25649bfba3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 13 Oct 2026 21:21:20 +0200 Subject: [PATCH] Add Short songs and a choice of how random songs are picked (#30, #51) Short songs (#30): a quiz settings switch and a Seconds per song row (30 by default, 10-120 in steps of 5), saved with the quiz settings. Only part of each quiz song plays, from a random point 10-60% into it, picked once per song so restart and repeat play the same part. The progress bar, time and seeking stay within the part, a hidden beginning counts inside it, and its end counts as the song ending (repeat, stop or go to the next song as set). Random songs (#51): the low-played switch in Settings is now a Normal / Low played / Classic choice, stored as Settings.random_mode and not saved. fetch_random_song(mode) picks /music/rand, /music/rand/low or /music/rand/classic. Closes #30 Closes #51 Co-Authored-By: Claude Opus 5.5 --- data/settings.gd | 13 +- data/song_object.gd | 3 + player/music_player.gd | 117 +- popups/SettingsPopup.tscn | 27 +- popups/settings_popup.gd | 16 +- quiz/QuizSettingsCard.tscn | 43 + quiz/quiz_settings_card.gd | 34 + quiz/song_loader.gd | 2 +- server/music_server_client.gd | 23 +- tests/data/layout_baseline.json | 1068 ++++++++++++++---- tests/fakes/main_window_test_suite.gd | 2 +- tests/fakes/ui_tour.gd | 2 +- tests/integration/test_main_window.gd | 2 +- tests/integration/test_quiz_settings_card.gd | 30 + tests/integration/test_settings_popup.gd | 33 +- tests/integration/test_short_songs.gd | 190 ++++ tests/integration/test_short_songs.gd.uid | 1 + tests/integration/test_song_loader.gd | 21 +- tests/unit/test_music_server_client.gd | 22 +- tests/unit/test_settings_files.gd | 3 +- 20 files changed, 1325 insertions(+), 327 deletions(-) create mode 100644 tests/integration/test_short_songs.gd create mode 100644 tests/integration/test_short_songs.gd.uid diff --git a/data/settings.gd b/data/settings.gd index 438d58b..3631a6f 100644 --- a/data/settings.gd +++ b/data/settings.gd @@ -6,6 +6,10 @@ extends Node ## Emitted when another theme is used, for code that reads theme colours itself. signal theme_changed +## How the server picks a random song: any soundtrack then any of its songs, preferring +## soundtracks played less than average, or every song with the same chance. +enum RandomMode { NORMAL, LOW_PLAYED, CLASSIC } + const MAX_NUMBER_OF_PLAYERS: int = 7 ## How many letters a search needs before it also finds names with the letters in order ## but apart, e.g. "lttp" for "A Link to the Past". Shorter searches only find names @@ -26,7 +30,7 @@ const SAVED_SETTINGS: Dictionary[String, Array] = { "quiz": [ &"auto_repeat_song", &"stop_after_current", &"hide_next_track", &"hide_beginning", &"hide_for_seconds", &"hide_length", &"hide_ticks", &"winning_score", - &"inspiration_list_speed", + &"inspiration_list_speed", &"short_songs", &"short_song_seconds", ], "window": [&"fullscreen"], "app": [&"last_seen_version", &"theme_id", &"reduce_animations"], @@ -77,7 +81,8 @@ var stop_after_current: bool = true var auto_repeat_song: bool = true var hide_next_track: bool = true var add_to_stats: bool = true -var use_low_played_mode: bool = false +## How the server picks random songs. Not saved, like statistics counting. +var random_mode: RandomMode = RandomMode.NORMAL var winning_score: int = 20 var fullscreen: bool = false var inspiration_list_speed: int = 1 @@ -85,6 +90,10 @@ var hide_for_seconds: float = 5.0 var hide_beginning: bool = false var hide_length: bool = false var hide_ticks: bool = false +## Plays only [member short_song_seconds] of each song, from a random point, so more +## songs fit in a game. +var short_songs: bool = false +var short_song_seconds: int = 30 var player_array: Array[PlayerObject] var edit_players: bool = false var currently_syncing: bool = false diff --git a/data/song_object.gd b/data/song_object.gd index b901617..7f9324d 100644 --- a/data/song_object.gd +++ b/data/song_object.gd @@ -17,6 +17,9 @@ var is_playing: bool = false var song_path: String = "" ## The song itself, only when it could not be saved to a file. var song: AudioStream +## Where the part played with Short songs starts, in seconds, or -1 before it is picked. +## Picked once, so restarting and repeating play the same part. +var short_start: float = -1.0 func get_soundtrack_title() -> String: diff --git a/player/music_player.gd b/player/music_player.gd index 7b3ce51..578f0a3 100644 --- a/player/music_player.gd +++ b/player/music_player.gd @@ -5,6 +5,11 @@ extends Control ## ## The buttons grow a little under the mouse and the play button pops when it switches ## between play and pause, see [ButtonBounce]. +## +## With Short songs on ([member Settings.short_songs]) only a part of each quiz song plays: +## [member Settings.short_song_seconds] from a random point between +## [constant SHORT_START_MIN] and [constant SHORT_START_MAX] of the song. The progress bar, +## the time and seeking all stay within that part, and its end counts as the song ending. signal update_song_list signal play_next_song @@ -16,6 +21,15 @@ var playback_position: float var stream: AudioStream var song_finished: bool = false +## Where in the song the part played with Short songs may start, as a share of its length. +## Many songs have a quiet intro, so the start is never right at the beginning. +const SHORT_START_MIN: float = 0.1 +const SHORT_START_MAX: float = 0.6 + +# The part of the song that plays: its start in seconds, and its length (0: to the end) +var _clip_start: float = 0.0 +var _clip_length: float = 0.0 + @onready var play_button: Button = %PlayButton @onready var restart_button: Button = %RestartButton @onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer @@ -38,14 +52,46 @@ func _ready() -> void: func _process(_delta: float) -> void: - if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused: - if !Settings.hide_beginning \ - || audio_player.get_playback_position() > Settings.hide_for_seconds: - progress_slider.value = audio_player.get_playback_position() - if stream != null: - music_time_label.text = format_text(progress_slider.value, stream.get_length()) - else: - music_time_label.text = format_text(progress_slider.value, 0.0) + if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused \ + && audio_player.playing: + var position: float = position_in_part() + if _clip_length > 0.0 && position >= _clip_length: + # The short part has played, which counts as the song ending + audio_player.stop() + progress_slider.value = _clip_length + _on_finished() + return + if !Settings.hide_beginning || position > Settings.hide_for_seconds: + progress_slider.value = position + music_time_label.text = format_text(progress_slider.value, part_length()) + + +## How long the part of the song that plays is: the short part, or the whole song. +func part_length() -> float: + if _clip_length > 0.0: + return _clip_length + return stream.get_length() if stream != null else 0.0 + + +## How far into the part that plays the song is, in seconds. +func position_in_part() -> float: + return audio_player.get_playback_position() - _clip_start + + +## Plays only part of [param song_object]'s song when Short songs is on and the song is +## longer than the part. The part's start is picked once per song. +func choose_part(song_object: SongObject, song: AudioStream) -> void: + _clip_start = 0.0 + _clip_length = 0.0 + var length: float = song.get_length() + if !Settings.short_songs || song_object == null || length <= Settings.short_song_seconds: + return + if song_object.short_start < 0.0: + var latest: float = minf(length * SHORT_START_MAX, length - Settings.short_song_seconds) + var earliest: float = minf(length * SHORT_START_MIN, latest) + song_object.short_start = randf_range(earliest, latest) + _clip_start = minf(song_object.short_start, length - Settings.short_song_seconds) + _clip_length = Settings.short_song_seconds ## Whether the play button shows pause, because a song is playing. @@ -61,6 +107,14 @@ func _show_playing(playing: bool) -> void: ButtonBounce.pop(play_button) +# The progress bar and time show the length of the part that plays +func _show_length() -> void: + var length: float = part_length() + progress_slider.max_value = round(length) + progress_slider.tick_count = 0 if Settings.hide_ticks else round(length / 60) + music_time_label.text = format_text(progress_slider.value, length) + + func format_time(time: float) -> String: var total_seconds: int = roundi(time) @warning_ignore("integer_division") @@ -87,11 +141,7 @@ func play_or_pause() -> void: song_finished = false audio_player.seek(playback_position) GameLog.debug("continue") - progress_slider.max_value = round(stream.get_length()) - if Settings.hide_ticks: - progress_slider.tick_count = 0 - else: - progress_slider.tick_count = round(stream.get_length() / 60) + _show_length() else: pause() @@ -102,15 +152,15 @@ func pause() -> void: _show_playing(false) +## Plays the song, or its short part, from the beginning. func restart() -> void: audio_player.stop() audio_player.stream_paused = false song_finished = false progress_slider.value = 0 - playback_position = audio_player.get_playback_position() - audio_player.seek(playback_position) + playback_position = _clip_start _show_playing(true) - audio_player.play() + audio_player.play(_clip_start) func _on_finished() -> void: @@ -127,16 +177,18 @@ func _on_drag_started() -> void: func _on_drag_ended(_changed: bool) -> void: - audio_player.seek(progress_slider.value) - playback_position = progress_slider.value + audio_player.seek(_clip_start + progress_slider.value) + playback_position = _clip_start + progress_slider.value is_changing = false +## Jumps [param new_position] seconds forward, or back when negative, staying within the +## part that plays. func seek(new_position: float) -> void: progress_slider.value += new_position is_changing = true - audio_player.seek(progress_slider.value) - playback_position = progress_slider.value + audio_player.seek(_clip_start + progress_slider.value) + playback_position = _clip_start + progress_slider.value is_changing = false @@ -173,6 +225,9 @@ func play_sound(sound_name: AudioStream) -> void: func play_song(song: AudioStream) -> void: + # Sounds such as the winning song always play whole + _clip_start = 0.0 + _clip_length = 0.0 audio_player.stream = song audio_player.play() sound_player.stop() @@ -180,12 +235,7 @@ func play_song(song: AudioStream) -> void: _show_playing(true) stream = audio_player.stream progress_slider.value = 0 - progress_slider.max_value = round(stream.get_length()) - if Settings.hide_ticks: - progress_slider.tick_count = 0 - else: - progress_slider.tick_count = round(stream.get_length() / 60) - music_time_label.text = format_text(progress_slider.value, stream.get_length()) + _show_length() ## Puts song [param song_object_no] in the player without playing it, e.g. the song that @@ -196,12 +246,11 @@ func load_song_object(song_object_no: int) -> void: return audio_player.stream = song stream = song + choose_part(Playlist.song_object_array[song_object_no], song) song_finished = true _show_playing(false) progress_slider.value = 0 - progress_slider.max_value = round(stream.get_length()) - progress_slider.tick_count = 0 if Settings.hide_ticks else round(stream.get_length() / 60) - music_time_label.text = format_text(progress_slider.value, stream.get_length()) + _show_length() func play_song_object(song_object_no: int) -> void: @@ -214,20 +263,16 @@ func play_song_object(song_object_no: int) -> void: audio_player.stop() await get_tree().create_timer(0.5).timeout audio_player.stream = song + choose_part(Playlist.song_object_array[song_object_no], song) sound_player.stop() - audio_player.play() + audio_player.play(_clip_start) song_finished = false _show_playing(true) # A new song makes the play button pop, also when it was already playing ButtonBounce.pop(play_button) stream = audio_player.stream progress_slider.value = 0 - progress_slider.max_value = round(stream.get_length()) - if Settings.hide_ticks: - progress_slider.tick_count = 0 - else: - progress_slider.tick_count = round(stream.get_length() / 60) - music_time_label.text = format_text(progress_slider.value, stream.get_length()) + _show_length() Playlist.set_currently_playing_song(song_object_no) Playlist.unset_is_playing() if !Settings.hide_next_track: diff --git a/popups/SettingsPopup.tscn b/popups/SettingsPopup.tscn index 7819438..cac3827 100644 --- a/popups/SettingsPopup.tscn +++ b/popups/SettingsPopup.tscn @@ -205,12 +205,31 @@ action_mode = 0 theme_type_variation = &"StepButton" text = "+1" -[node name="LowPlayedCheckButton" type="CheckButton" parent="SettingsPanel/VBoxContainer"] -unique_name_in_owner = true +[node name="RandomModeRow" type="HBoxContainer" parent="SettingsPanel/VBoxContainer"] layout_mode = 2 + +[node name="Title" type="Label" parent="SettingsPanel/VBoxContainer/RandomModeRow"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"SettingsRowLabel" +text = "Random songs" + +[node name="RandomModeButton" type="OptionButton" parent="SettingsPanel/VBoxContainer/RandomModeRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(180, 0) +layout_mode = 2 +tooltip_text = "Normal: a random soundtrack, then one of its songs. +Low played: the same, but soundtracks played less than average come up more often. +Classic: every song has the same chance, whatever its soundtrack." focus_mode = 0 -action_mode = 0 -text = "Prefer less played soundtracks" +selected = 0 +item_count = 3 +popup/item_0/text = "Normal" +popup/item_0/id = 0 +popup/item_1/text = "Low played" +popup/item_1/id = 1 +popup/item_2/text = "Classic" +popup/item_2/id = 2 [node name="AddToDatabaseCheckButton" type="CheckButton" parent="SettingsPanel/VBoxContainer"] unique_name_in_owner = true diff --git a/popups/settings_popup.gd b/popups/settings_popup.gd index d430506..9acd09e 100644 --- a/popups/settings_popup.gd +++ b/popups/settings_popup.gd @@ -1,9 +1,9 @@ class_name SettingsPopup extends BasePopup ## The application settings: which MusicServer or music folder to play from, the theme, -## fullscreen, fewer animations, how many songs to download ahead, whether to prefer less played soundtracks -## and whether plays count in the statistics, and a button that opens the folder the -## settings are kept in. Opened from the Settings button in the top bar. +## fullscreen, fewer animations, how many songs to download ahead, how random songs are +## picked and whether plays count in the statistics, and a button that opens the folder +## the settings are kept in. Opened from the Settings button in the top bar. ## ## A music folder (see [MusicFolder]) is used instead of the server until Use server is ## pressed or the game is closed. The settings that need the server are off meanwhile. @@ -30,7 +30,7 @@ const PADDING: Vector2 = Vector2(20, 12) @onready var cache_label: Label = %CacheLabel @onready var lower_cache_button: Button = %LowerCacheButton @onready var increase_cache_button: Button = %IncreaseCacheButton -@onready var low_played_button: CheckButton = %LowPlayedCheckButton +@onready var random_mode_button: OptionButton = %RandomModeButton @onready var add_to_database_button: CheckButton = %AddToDatabaseCheckButton @onready var rows: VBoxContainer = %VBoxContainer @onready var folder_label: Label = %FolderLabel @@ -81,8 +81,8 @@ func _ready() -> void: lower_cache_button.pressed.connect(lower_cache) increase_cache_button.pressed.connect(increase_cache) select_server_button.item_selected.connect(select_server) - low_played_button.toggled.connect( - func(on: bool) -> void: Settings.use_low_played_mode = on) + random_mode_button.item_selected.connect( + func(mode: int) -> void: Settings.random_mode = mode as Settings.RandomMode) add_to_database_button.toggled.connect(func(on: bool) -> void: Settings.add_to_stats = on) visibility_changed.connect(_on_visibility_changed) # Alt+Enter switches fullscreen from the main window, which resizes the window @@ -189,7 +189,7 @@ func _use_server(new_server: int) -> void: func _on_visibility_changed() -> void: if visible: _update_fullscreen_button() - low_played_button.set_pressed_no_signal(Settings.use_low_played_mode) + random_mode_button.select(Settings.random_mode) add_to_database_button.set_pressed_no_signal(Settings.add_to_stats) else: folder_error_label.visible = false @@ -224,7 +224,7 @@ func _show_music_source() -> void: characters_label.text = "From " + folder.display_name() else: characters_label.text = "From the server" - for control: BaseButton in [select_server_button, low_played_button, add_to_database_button]: + for control: BaseButton in [select_server_button, random_mode_button, add_to_database_button]: control.disabled = folder != null _fit_to_rows() diff --git a/quiz/QuizSettingsCard.tscn b/quiz/QuizSettingsCard.tscn index de026d8..9f08569 100644 --- a/quiz/QuizSettingsCard.tscn +++ b/quiz/QuizSettingsCard.tscn @@ -50,6 +50,49 @@ focus_mode = 0 action_mode = 0 text = "Hide next track" +[node name="ShortSongsCheckButton" type="CheckButton" parent="CardPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Play only a part of each song, from a random point, so more songs fit in a game" +focus_mode = 0 +action_mode = 0 +text = "Short songs" + +[node name="ShortSongSecondsRow" type="HBoxContainer" parent="CardPanel/VBox"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Title" type="Label" parent="CardPanel/VBox/ShortSongSecondsRow"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"SettingsRowLabel" +text = "Seconds per song" + +[node name="ShortSongSecondsLabel" type="Label" parent="CardPanel/VBox/ShortSongSecondsRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(48, 0) +layout_mode = 2 +text = "30" +horizontal_alignment = 1 + +[node name="LowerShortSongSecondsButton" type="Button" parent="CardPanel/VBox/ShortSongSecondsRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(30, 0) +layout_mode = 2 +focus_mode = 0 +action_mode = 0 +theme_type_variation = &"StepButton" +text = "-5" + +[node name="IncreaseShortSongSecondsButton" type="Button" parent="CardPanel/VBox/ShortSongSecondsRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(30, 0) +layout_mode = 2 +focus_mode = 0 +action_mode = 0 +theme_type_variation = &"StepButton" +text = "+5" + [node name="HideBeginningCheckButton" type="CheckButton" parent="CardPanel/VBox"] unique_name_in_owner = true layout_mode = 2 diff --git a/quiz/quiz_settings_card.gd b/quiz/quiz_settings_card.gd index 900f140..a778056 100644 --- a/quiz/quiz_settings_card.gd +++ b/quiz/quiz_settings_card.gd @@ -13,6 +13,10 @@ extends BasePopup ## The most seconds of a song's beginning that can be hidden. const MAX_HIDE_SECONDS: float = 10.0 +## The shortest and longest part of a song that Short songs plays, and the step between. +const MIN_SHORT_SONG_SECONDS: int = 10 +const MAX_SHORT_SONG_SECONDS: int = 120 +const SHORT_SONG_STEP: int = 5 ## Space between the rows and the card's edges. const PADDING: Vector2 = Vector2(16, 12) @@ -21,6 +25,11 @@ const PADDING: Vector2 = Vector2(16, 12) @onready var auto_repeat_button: CheckButton = %AutoRepeatCheckButton @onready var stop_after_current_button: CheckButton = %StopAfterCurrentCheckButton @onready var hide_next_track_button: CheckButton = %HideNextTrackCheckButton +@onready var short_songs_button: CheckButton = %ShortSongsCheckButton +@onready var short_song_seconds_row: HBoxContainer = %ShortSongSecondsRow +@onready var short_song_seconds_label: Label = %ShortSongSecondsLabel +@onready var lower_short_song_seconds_button: Button = %LowerShortSongSecondsButton +@onready var increase_short_song_seconds_button: Button = %IncreaseShortSongSecondsButton @onready var hide_beginning_button: CheckButton = %HideBeginningCheckButton @onready var hide_for_seconds_row: HBoxContainer = %HideForSecondsRow @onready var hide_for_seconds_label: Label = %HideForSecondsLabel @@ -41,6 +50,11 @@ func _ready() -> void: auto_repeat_button.toggled.connect(_save_toggle.bind(&"auto_repeat_song")) stop_after_current_button.toggled.connect(_save_toggle.bind(&"stop_after_current")) hide_next_track_button.toggled.connect(_save_toggle.bind(&"hide_next_track")) + short_songs_button.toggled.connect(_on_short_songs_toggled) + lower_short_song_seconds_button.pressed.connect( + set_short_song_seconds.bind(-SHORT_SONG_STEP, true)) + increase_short_song_seconds_button.pressed.connect( + set_short_song_seconds.bind(SHORT_SONG_STEP, true)) hide_beginning_button.toggled.connect(_on_hide_beginning_toggled) hide_length_button.toggled.connect(_save_toggle.bind(&"hide_length")) hide_ticks_button.toggled.connect(_save_toggle.bind(&"hide_ticks")) @@ -60,6 +74,9 @@ func show_settings() -> void: auto_repeat_button.set_pressed_no_signal(Settings.auto_repeat_song) stop_after_current_button.set_pressed_no_signal(Settings.stop_after_current) hide_next_track_button.set_pressed_no_signal(Settings.hide_next_track) + short_songs_button.set_pressed_no_signal(Settings.short_songs) + short_song_seconds_row.visible = Settings.short_songs + short_song_seconds_label.text = str(Settings.short_song_seconds) hide_beginning_button.set_pressed_no_signal(Settings.hide_beginning) hide_length_button.set_pressed_no_signal(Settings.hide_length) hide_ticks_button.set_pressed_no_signal(Settings.hide_ticks) @@ -117,6 +134,23 @@ func _save_toggle(on: bool, setting: StringName) -> void: Settings.save_settings() +## Sets how many seconds of each song Short songs plays, between +## [constant MIN_SHORT_SONG_SECONDS] and [constant MAX_SHORT_SONG_SECONDS]. With +## [param relative] the seconds are added to the current length. +func set_short_song_seconds(seconds: int, relative: bool = false) -> void: + var length: int = Settings.short_song_seconds + seconds if relative else seconds + Settings.short_song_seconds = clampi(length, MIN_SHORT_SONG_SECONDS, + MAX_SHORT_SONG_SECONDS) + short_song_seconds_label.text = str(Settings.short_song_seconds) + Settings.save_settings() + + +func _on_short_songs_toggled(on: bool) -> void: + _save_toggle(on, &"short_songs") + short_song_seconds_row.visible = on + _fit_to_rows() + + func _on_hide_beginning_toggled(on: bool) -> void: _save_toggle(on, &"hide_beginning") hide_for_seconds_row.visible = on diff --git a/quiz/song_loader.gd b/quiz/song_loader.gd index 2c1a20e..b0274b6 100644 --- a/quiz/song_loader.gd +++ b/quiz/song_loader.gd @@ -122,7 +122,7 @@ func _download_random_song(new_song_object: SongObject) -> void: return Playlist.store_song(new_song_object, body) _add_song_to_queue(new_song_object) - MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded, + MusicServerClient.fetch_random_song(Settings.random_mode, song_downloaded, _retry_later.bind(_download_random_song.bind(new_song_object))) diff --git a/server/music_server_client.gd b/server/music_server_client.gd index 40e21d5..01d59e6 100644 --- a/server/music_server_client.gd +++ b/server/music_server_client.gd @@ -51,17 +51,22 @@ func fetch_song_list(on_done: Callable, on_failed: Callable = Callable()) -> voi get_json_list("/music/list", SongInfo, on_done, on_failed) -## Downloads a random song as MP3 bytes. With [param low_played] the server prefers -## soundtracks that have been played the least. -func fetch_random_song( - low_played: bool, on_done: Callable, on_failed: Callable = Callable()) -> void: - get_bytes("/music/rand/low" if low_played else "/music/rand", on_done, on_failed) +## Downloads a random song as MP3 bytes, picked the way [param mode] (a +## [code]Settings.RandomMode[/code]) says: a random soundtrack and then one of its songs, +## the same but preferring soundtracks played less than average, or every song with the +## same chance. +func fetch_random_song(mode: int, on_done: Callable, on_failed: Callable = Callable()) -> void: + get_bytes(random_song_path(mode), on_done, on_failed) -## Downloads a random song as MP3 bytes, picked the way the server did before low played -## mode existed: any soundtrack is as likely as any other. -func fetch_random_song_classic(on_done: Callable, on_failed: Callable = Callable()) -> void: - get_bytes("/music/rand/classic", on_done, on_failed) +## The endpoint for random songs picked the way [param mode] says. +static func random_song_path(mode: int) -> String: + match mode: + Settings.RandomMode.LOW_PLAYED: + return "/music/rand/low" + Settings.RandomMode.CLASSIC: + return "/music/rand/classic" + return "/music/rand" ## Adds the most recently fetched random song to the server playlist. diff --git a/tests/data/layout_baseline.json b/tests/data/layout_baseline.json index 16d380e..d94ed2b 100644 --- a/tests/data/layout_baseline.json +++ b/tests/data/layout_baseline.json @@ -1299,25 +1299,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 140.0, 31.0 ], @@ -1326,7 +1326,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideBeginningCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 263.0, 31.0 ], @@ -1335,7 +1335,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -1344,7 +1344,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -1353,7 +1353,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1362,7 +1362,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1371,7 +1371,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -1380,7 +1380,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideLengthCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 178.0, 31.0 ], @@ -1389,7 +1389,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 167.0, 31.0 ], @@ -1398,7 +1398,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideTicksCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 235.0, 31.0 ], @@ -1407,7 +1407,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow": { "rect": [ 80.0, - 635.0, + 600.0, 294.0, 31.0 ], @@ -1416,7 +1416,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/IncreaseInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1425,7 +1425,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/InspirationListSpeedLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -1434,7 +1434,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/LowerInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1443,16 +1443,70 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 174.0, 31.0 ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 600.0, + 139.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 187.0, 31.0 ], @@ -1461,7 +1515,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 156.0, 35.0 ], @@ -1470,7 +1524,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow": { "rect": [ 80.0, - 635.0, + 600.0, 242.0, 31.0 ], @@ -1479,7 +1533,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/IncreaseButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1488,7 +1542,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/LowerButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -1497,7 +1551,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/ScoreLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -1506,7 +1560,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 122.0, 31.0 ], @@ -2304,15 +2358,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 347.0, - 285.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -2340,6 +2385,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 347.0, + 310.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 720.0, + 347.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 347.0, + 126.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -5495,25 +5567,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 674.0, + 639.0, 408.0, 31.0 ], @@ -5531,7 +5603,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -5540,7 +5612,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -5549,7 +5621,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -5558,7 +5630,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -5567,7 +5639,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -5585,7 +5657,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 744.0, + 709.0, 408.0, 31.0 ], @@ -5645,10 +5717,64 @@ ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 744.0, + 408.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 709.0, + 674.0, 408.0, 31.0 ], @@ -5657,7 +5783,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, 35.0 ], @@ -6545,15 +6671,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 631.0, - 480.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -6581,6 +6698,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 631.0, + 480.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 1020.0, + 631.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 631.0, + 296.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -12049,25 +12193,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 140.0, 31.0 ], @@ -12076,7 +12220,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideBeginningCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 263.0, 31.0 ], @@ -12085,7 +12229,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -12094,7 +12238,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -12103,7 +12247,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12112,7 +12256,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12121,7 +12265,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -12130,7 +12274,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideLengthCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 178.0, 31.0 ], @@ -12139,7 +12283,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 167.0, 31.0 ], @@ -12148,7 +12292,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideTicksCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 235.0, 31.0 ], @@ -12157,7 +12301,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow": { "rect": [ 80.0, - 635.0, + 600.0, 294.0, 31.0 ], @@ -12166,7 +12310,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/IncreaseInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12175,7 +12319,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/InspirationListSpeedLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -12184,7 +12328,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/LowerInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12193,16 +12337,70 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 174.0, 31.0 ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 600.0, + 139.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 187.0, 31.0 ], @@ -12211,7 +12409,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 156.0, 35.0 ], @@ -12220,7 +12418,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow": { "rect": [ 80.0, - 635.0, + 600.0, 242.0, 31.0 ], @@ -12229,7 +12427,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/IncreaseButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12238,7 +12436,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/LowerButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -12247,7 +12445,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/ScoreLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -12256,7 +12454,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 122.0, 31.0 ], @@ -13054,15 +13252,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 347.0, - 285.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -13090,6 +13279,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 347.0, + 310.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 720.0, + 347.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 347.0, + 126.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -16128,25 +16344,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 140.0, 31.0 ], @@ -16155,7 +16371,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideBeginningCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 263.0, 31.0 ], @@ -16164,7 +16380,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -16173,7 +16389,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -16182,7 +16398,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16191,7 +16407,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16200,7 +16416,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -16209,7 +16425,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideLengthCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 178.0, 31.0 ], @@ -16218,7 +16434,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 167.0, 31.0 ], @@ -16227,7 +16443,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideTicksCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 235.0, 31.0 ], @@ -16236,7 +16452,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow": { "rect": [ 80.0, - 635.0, + 600.0, 294.0, 31.0 ], @@ -16245,7 +16461,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/IncreaseInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16254,7 +16470,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/InspirationListSpeedLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -16263,7 +16479,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/LowerInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16272,16 +16488,70 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 174.0, 31.0 ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 600.0, + 139.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 187.0, 31.0 ], @@ -16290,7 +16560,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 156.0, 35.0 ], @@ -16299,7 +16569,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow": { "rect": [ 80.0, - 635.0, + 600.0, 242.0, 31.0 ], @@ -16308,7 +16578,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/IncreaseButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16317,7 +16587,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/LowerButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -16326,7 +16596,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/ScoreLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -16335,7 +16605,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 122.0, 31.0 ], @@ -17133,15 +17403,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 347.0, - 285.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -17169,6 +17430,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 347.0, + 310.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 720.0, + 347.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 347.0, + 126.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -19441,25 +19729,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": true }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": true }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 674.0, + 639.0, 408.0, 31.0 ], @@ -19477,7 +19765,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -19486,7 +19774,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -19495,7 +19783,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -19504,7 +19792,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -19513,7 +19801,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -19531,7 +19819,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 744.0, + 709.0, 408.0, 31.0 ], @@ -19591,10 +19879,64 @@ ], "visible": true }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 744.0, + 408.0, + 31.0 + ], + "visible": true + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 709.0, + 674.0, 408.0, 31.0 ], @@ -19603,7 +19945,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, 35.0 ], @@ -20434,15 +20776,6 @@ ], "visible": true }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 631.0, - 480.0, - 31.0 - ], - "visible": true - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -20470,6 +20803,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 631.0, + 480.0, + 31.0 + ], + "visible": true + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 1020.0, + 631.0, + 180.0, + 31.0 + ], + "visible": true + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 631.0, + 296.0, + 31.0 + ], + "visible": true + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -20787,15 +21147,6 @@ ], "visible": true }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 649.0, - 480.0, - 31.0 - ], - "visible": true - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -20823,6 +21174,33 @@ ], "visible": true }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 649.0, + 480.0, + 31.0 + ], + "visible": true + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 1020.0, + 649.0, + 180.0, + 31.0 + ], + "visible": true + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 649.0, + 296.0, + 31.0 + ], + "visible": true + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -24287,25 +24665,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 140.0, 31.0 ], @@ -24314,7 +24692,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideBeginningCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 263.0, 31.0 ], @@ -24323,7 +24701,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -24332,7 +24710,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -24341,7 +24719,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24350,7 +24728,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24359,7 +24737,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -24368,7 +24746,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideLengthCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 178.0, 31.0 ], @@ -24377,7 +24755,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 167.0, 31.0 ], @@ -24386,7 +24764,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideTicksCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 235.0, 31.0 ], @@ -24395,7 +24773,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow": { "rect": [ 80.0, - 635.0, + 600.0, 294.0, 31.0 ], @@ -24404,7 +24782,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/IncreaseInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24413,7 +24791,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/InspirationListSpeedLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -24422,7 +24800,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/LowerInspirationSpeedButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24431,16 +24809,70 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/InspirationRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 174.0, 31.0 ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 600.0, + 139.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 635.0, + 600.0, 187.0, 31.0 ], @@ -24449,7 +24881,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 156.0, 35.0 ], @@ -24458,7 +24890,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow": { "rect": [ 80.0, - 635.0, + 600.0, 242.0, 31.0 ], @@ -24467,7 +24899,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/IncreaseButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24476,7 +24908,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/LowerButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -24485,7 +24917,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/ScoreLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -24494,7 +24926,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/WinningScoreRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 122.0, 31.0 ], @@ -25292,15 +25724,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 347.0, - 285.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -25328,6 +25751,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 347.0, + 310.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 720.0, + 347.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 347.0, + 126.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -37691,25 +38141,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 674.0, + 639.0, 408.0, 31.0 ], @@ -37727,7 +38177,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -37736,7 +38186,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -37745,7 +38195,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -37754,7 +38204,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -37763,7 +38213,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -37781,7 +38231,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 744.0, + 709.0, 408.0, 31.0 ], @@ -37841,10 +38291,64 @@ ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 744.0, + 408.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 709.0, + 674.0, 408.0, 31.0 ], @@ -37853,7 +38357,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, 35.0 ], @@ -38759,15 +39263,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 631.0, - 480.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -38795,6 +39290,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 631.0, + 480.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 1020.0, + 631.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 631.0, + 296.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, @@ -42368,25 +42890,25 @@ "Quiz/QuizSettingsCard/CardPanel": { "rect": [ 64.0, - 623.0, + 588.0, 440.0, - 339.0 + 374.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, - 315.0 + 350.0 ], "visible": false }, "Quiz/QuizSettingsCard/CardPanel/VBox/AutoRepeatCheckButton": { "rect": [ 80.0, - 674.0, + 639.0, 408.0, 31.0 ], @@ -42404,7 +42926,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow": { "rect": [ 80.0, - 635.0, + 600.0, 254.0, 31.0 ], @@ -42413,7 +42935,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/HideForSecondsLabel": { "rect": [ 80.0, - 635.0, + 600.0, 48.0, 23.0 ], @@ -42422,7 +42944,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/IncreaseHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -42431,7 +42953,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/LowerHideForSecondsButton": { "rect": [ 80.0, - 635.0, + 600.0, 30.0, 31.0 ], @@ -42440,7 +42962,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideForSecondsRow/Title": { "rect": [ 80.0, - 635.0, + 600.0, 134.0, 31.0 ], @@ -42458,7 +42980,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/HideNextTrackCheckButton": { "rect": [ 80.0, - 744.0, + 709.0, 408.0, 31.0 ], @@ -42518,10 +43040,64 @@ ], "visible": false }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow": { + "rect": [ + 80.0, + 600.0, + 267.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/IncreaseShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/LowerShortSongSecondsButton": { + "rect": [ + 80.0, + 600.0, + 30.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/ShortSongSecondsLabel": { + "rect": [ + 80.0, + 600.0, + 48.0, + 23.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongSecondsRow/Title": { + "rect": [ + 80.0, + 600.0, + 147.0, + 31.0 + ], + "visible": false + }, + "Quiz/QuizSettingsCard/CardPanel/VBox/ShortSongsCheckButton": { + "rect": [ + 80.0, + 744.0, + 408.0, + 31.0 + ], + "visible": false + }, "Quiz/QuizSettingsCard/CardPanel/VBox/StopAfterCurrentCheckButton": { "rect": [ 80.0, - 709.0, + 674.0, 408.0, 31.0 ], @@ -42530,7 +43106,7 @@ "Quiz/QuizSettingsCard/CardPanel/VBox/TitleLabel": { "rect": [ 80.0, - 635.0, + 600.0, 408.0, 35.0 ], @@ -43436,15 +44012,6 @@ ], "visible": false }, - "SettingsPopup/SettingsPanel/VBoxContainer/LowPlayedCheckButton": { - "rect": [ - 720.0, - 631.0, - 480.0, - 31.0 - ], - "visible": false - }, "SettingsPopup/SettingsPanel/VBoxContainer/PlayOrderRow": { "rect": [ 720.0, @@ -43472,6 +44039,33 @@ ], "visible": false }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow": { + "rect": [ + 720.0, + 631.0, + 480.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/RandomModeButton": { + "rect": [ + 1020.0, + 631.0, + 180.0, + 31.0 + ], + "visible": false + }, + "SettingsPopup/SettingsPanel/VBoxContainer/RandomModeRow/Title": { + "rect": [ + 720.0, + 631.0, + 296.0, + 31.0 + ], + "visible": false + }, "SettingsPopup/SettingsPanel/VBoxContainer/ReduceAnimationsButton": { "rect": [ 720.0, diff --git a/tests/fakes/main_window_test_suite.gd b/tests/fakes/main_window_test_suite.gd index 964bc59..de3c131 100644 --- a/tests/fakes/main_window_test_suite.gd +++ b/tests/fakes/main_window_test_suite.gd @@ -26,7 +26,7 @@ func before_test() -> void: QuizSave.delete() Settings.add_to_stats = true Settings.hide_next_track = true - Settings.use_low_played_mode = false + Settings.random_mode = Settings.RandomMode.NORMAL Settings.winning_score = 20 # Otherwise the About popup opens at start, as for a new version Settings.last_seen_version = Settings.version diff --git a/tests/fakes/ui_tour.gd b/tests/fakes/ui_tour.gd index 920a78e..d6fb3f0 100644 --- a/tests/fakes/ui_tour.gd +++ b/tests/fakes/ui_tour.gd @@ -172,7 +172,7 @@ static func prepare_autoloads() -> void: Settings.character_select_open = false Settings.add_to_stats = true Settings.hide_next_track = true - Settings.use_low_played_mode = false + Settings.random_mode = Settings.RandomMode.NORMAL Settings.winning_score = 20 Settings.last_seen_version = Settings.version # The same picture every time, without half-finished animations diff --git a/tests/integration/test_main_window.gd b/tests/integration/test_main_window.gd index ec0e84b..aa5e2c3 100644 --- a/tests/integration/test_main_window.gd +++ b/tests/integration/test_main_window.gd @@ -11,7 +11,7 @@ func test_startup_loads_soundtracks_and_preloads_a_song() -> void: func test_low_played_mode_fetches_from_low_played_endpoint() -> void: - Settings.use_low_played_mode = true + Settings.random_mode = Settings.RandomMode.LOW_PLAYED _server.respond_bytes( "/music/rand/low", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) diff --git a/tests/integration/test_quiz_settings_card.gd b/tests/integration/test_quiz_settings_card.gd index f242a73..58dedc4 100644 --- a/tests/integration/test_quiz_settings_card.gd +++ b/tests/integration/test_quiz_settings_card.gd @@ -177,3 +177,33 @@ func test_changes_are_saved_at_once() -> void: assert_int(saved.load(Settings.saved_settings_path())).is_equal(OK) assert_bool(saved.get_value("quiz", "hide_next_track")).is_false() assert_int(saved.get_value("quiz", "winning_score")).is_equal(19) + + +func test_short_songs_shows_its_seconds_and_is_saved() -> void: + Settings.short_songs = false + await _open() + assert_bool(_card.short_song_seconds_row.visible).is_false() + + _card.short_songs_button.button_pressed = true + + assert_bool(Settings.short_songs).is_true() + assert_bool(_card.short_song_seconds_row.visible).is_true() + var saved: ConfigFile = ConfigFile.new() + assert_int(saved.load(Settings.saved_settings_path())).is_equal(OK) + assert_bool(saved.get_value("quiz", "short_songs")).is_true() + + +func test_seconds_per_song_go_in_fives_between_ten_and_two_minutes() -> void: + Settings.short_song_seconds = 30 + Settings.short_songs = true + await _open() + + _card.increase_short_song_seconds_button.pressed.emit() + assert_int(Settings.short_song_seconds).is_equal(35) + assert_str(_card.short_song_seconds_label.text).is_equal("35") + for i: int in 10: + _card.lower_short_song_seconds_button.pressed.emit() + assert_int(Settings.short_song_seconds).is_equal(QuizSettingsCard.MIN_SHORT_SONG_SECONDS) + for i: int in 40: + _card.increase_short_song_seconds_button.pressed.emit() + assert_int(Settings.short_song_seconds).is_equal(QuizSettingsCard.MAX_SHORT_SONG_SECONDS) diff --git a/tests/integration/test_settings_popup.gd b/tests/integration/test_settings_popup.gd index 670faf6..9dc07bc 100644 --- a/tests/integration/test_settings_popup.gd +++ b/tests/integration/test_settings_popup.gd @@ -9,15 +9,33 @@ func before_test() -> void: _popup = _window.get_node("SettingsPopup") -func test_less_played_toggle_changes_the_setting() -> void: - Settings.use_low_played_mode = false +func test_random_songs_choice_lists_the_three_modes() -> void: + var names: Array[String] = [] + for index: int in _popup.random_mode_button.item_count: + names.append(_popup.random_mode_button.get_item_text(index)) + + assert_array(names).contains_exactly(["Normal", "Low played", "Classic"]) + + +@warning_ignore("unused_parameter") +func test_random_songs_choice_changes_the_setting(mode: Settings.RandomMode, + test_parameters: Array = [[Settings.RandomMode.LOW_PLAYED], + [Settings.RandomMode.CLASSIC], [Settings.RandomMode.NORMAL]]) -> void: _popup.open() await _runner.simulate_frames(1) - assert_bool(_popup.low_played_button.button_pressed).is_false() - await _click_at(_popup.low_played_button.get_global_rect().get_center()) + _popup.random_mode_button.select(mode) + _popup.random_mode_button.item_selected.emit(mode) - assert_bool(Settings.use_low_played_mode).is_true() + assert_int(Settings.random_mode).is_equal(mode) + + +func test_random_songs_choice_shows_the_mode_in_use() -> void: + Settings.random_mode = Settings.RandomMode.CLASSIC + + _popup.open() + + assert_int(_popup.random_mode_button.selected).is_equal(Settings.RandomMode.CLASSIC) func test_statistics_toggle_changes_the_setting() -> void: @@ -33,14 +51,15 @@ func test_statistics_toggle_changes_the_setting() -> void: func test_statistics_settings_are_not_saved_with_the_quiz_settings() -> void: Settings.add_to_stats = false - Settings.use_low_played_mode = true + Settings.random_mode = Settings.RandomMode.LOW_PLAYED Settings.save_settings() var saved: ConfigFile = ConfigFile.new() assert_int(saved.load(Settings.saved_settings_path())).is_equal(OK) assert_bool(saved.has_section_key("quiz", "add_to_stats")).is_false() - assert_bool(saved.has_section_key("quiz", "use_low_played_mode")).is_false() + assert_bool(saved.has_section_key("quiz", "random_mode")).is_false() + assert_bool(saved.has_section_key("app", "random_mode")).is_false() func test_popup_fits_its_rows_and_is_centred() -> void: diff --git a/tests/integration/test_short_songs.gd b/tests/integration/test_short_songs.gd new file mode 100644 index 0000000..91bda1f --- /dev/null +++ b/tests/integration/test_short_songs.gd @@ -0,0 +1,190 @@ +extends GdUnitTestSuite +## Short songs: only a part of each quiz song plays, from a random point picked once per +## song, and the end of the part counts as the song ending. + +## How long the test song is, in seconds. +const SONG_SECONDS: float = 60.0 + +var _snapshot: AutoloadSnapshot +var _runner: GdUnitSceneRunner +var _player: MusicPlayer + + +func before_test() -> void: + _snapshot = AutoloadSnapshot.take([Settings, Playlist]) + Settings.short_songs = true + Settings.short_song_seconds = 20 + Settings.hide_beginning = false + Settings.hide_length = false + Settings.auto_repeat_song = false + Settings.stop_after_current = true + Playlist.song_object_array.clear() + _runner = scene_runner("res://player/MusicPlayer.tscn") + _player = _runner.scene() + + +func after_test() -> void: + _player.audio_player.stop() + _snapshot.restore() + + +# A silent song of [param seconds], in the playlist +func _add_song(seconds: float = SONG_SECONDS) -> SongObject: + var song: AudioStreamWAV = AudioStreamWAV.new() + song.format = AudioStreamWAV.FORMAT_8_BITS + song.mix_rate = 8000 + var silence: PackedByteArray = [] + silence.resize(int(seconds * song.mix_rate)) + song.data = silence + var song_object: SongObject = SongObject.new() + song_object.song = song + song_object.song_number = Playlist.song_object_array.size() + Playlist.add_to_playlist(song_object) + return song_object + + +func _play(song_object: SongObject) -> void: + await _player.play_song_object(song_object.song_number) + await _runner.simulate_frames(2) + + +func test_part_starts_between_ten_and_sixty_percent_in() -> void: + for i: int in 20: + var song_object: SongObject = _add_song() + _player.choose_part(song_object, song_object.song) + + assert_float(song_object.short_start).is_between(SONG_SECONDS * 0.1, SONG_SECONDS * 0.6) + assert_float(_player.part_length()).is_equal(20.0) + + +func test_part_is_picked_once_per_song() -> void: + var song_object: SongObject = _add_song() + _player.choose_part(song_object, song_object.song) + var start: float = song_object.short_start + + _player.choose_part(song_object, song_object.song) + + assert_float(song_object.short_start).is_equal(start) + + +func test_part_always_fits_in_the_song() -> void: + Settings.short_song_seconds = 50 + for i: int in 20: + var song_object: SongObject = _add_song() + _player.choose_part(song_object, song_object.song) + + assert_float(song_object.short_start + 50).is_less_equal(SONG_SECONDS) + + +func test_song_shorter_than_the_part_plays_whole() -> void: + var song_object: SongObject = _add_song(15.0) + + await _play(song_object) + + assert_float(_player.part_length()).is_equal_approx(15.0, 0.01) + assert_float(song_object.short_start).is_equal(-1.0) + + +func test_whole_song_plays_with_short_songs_off() -> void: + Settings.short_songs = false + var song_object: SongObject = _add_song() + + await _play(song_object) + + assert_float(_player.part_length()).is_equal_approx(SONG_SECONDS, 0.01) + assert_float(_player.position_in_part()).is_less(1.0) + + +func test_song_starts_at_its_part_and_shows_the_part_length() -> void: + var song_object: SongObject = _add_song() + + await _play(song_object) + + assert_float(_player.audio_player.get_playback_position()) \ + .is_greater_equal(song_object.short_start - 0.01) + assert_float(_player.position_in_part()).is_less(1.0) + assert_float(_player.progress_slider.max_value).is_equal(20.0) + assert_str(_player.music_time_label.text).ends_with("/ 00:20") + + +func test_seeking_stays_inside_the_part() -> void: + var song_object: SongObject = _add_song() + await _play(song_object) + + _player.seek(500) + + assert_float(_player.progress_slider.value).is_equal(20.0) + assert_float(_player.playback_position) \ + .is_equal_approx(song_object.short_start + 20.0, 0.01) + + +func test_end_of_the_part_stops_the_song() -> void: + var song_object: SongObject = _add_song() + await _play(song_object) + + _player.seek(19.9) + await await_millis(300) + + assert_bool(_player.song_finished).is_true() + assert_bool(_player.audio_player.playing).is_false() + + +func test_end_of_the_part_repeats_it_with_auto_repeat() -> void: + Settings.auto_repeat_song = true + var song_object: SongObject = _add_song() + await _play(song_object) + + _player.seek(19.9) + await await_millis(300) + + assert_bool(_player.audio_player.playing).is_true() + assert_float(_player.position_in_part()).is_less(1.0) + assert_float(_player.audio_player.get_playback_position()) \ + .is_greater_equal(song_object.short_start - 0.01) + + +func test_end_of_the_part_goes_to_the_next_song_without_stop_after_current() -> void: + Settings.stop_after_current = false + var song_object: SongObject = _add_song() + await _play(song_object) + var signals: GdUnitSignalAssert = assert_signal(monitor_signals(_player)) + + _player.seek(19.9) + + await signals.wait_until(1000).is_emitted("play_next_song") + + +func test_restart_plays_the_same_part_again() -> void: + var song_object: SongObject = _add_song() + await _play(song_object) + _player.seek(10) + + _player.restart() + await _runner.simulate_frames(2) + + assert_float(_player.position_in_part()).is_less(1.0) + assert_float(_player.audio_player.get_playback_position()) \ + .is_greater_equal(song_object.short_start - 0.01) + + +func test_hidden_beginning_counts_inside_the_part() -> void: + Settings.hide_beginning = true + Settings.hide_for_seconds = 5.0 + var song_object: SongObject = _add_song() + + await _play(song_object) + + assert_str(_player.music_time_label.text).is_equal("??:?? / 00:20") + _player.seek(6) + await _runner.simulate_frames(2) + assert_str(_player.music_time_label.text).starts_with("00:06") + + +func test_other_sounds_always_play_whole() -> void: + var song_object: SongObject = _add_song() + await _play(song_object) + + _player.play_song(_add_song(30.0).song) + + assert_float(_player.part_length()).is_equal_approx(30.0, 0.01) + assert_float(_player.position_in_part()).is_less(1.0) diff --git a/tests/integration/test_short_songs.gd.uid b/tests/integration/test_short_songs.gd.uid new file mode 100644 index 0000000..760fc3e --- /dev/null +++ b/tests/integration/test_short_songs.gd.uid @@ -0,0 +1 @@ +uid://ov2rqiwl8x6s diff --git a/tests/integration/test_song_loader.gd b/tests/integration/test_song_loader.gd index c59bbf5..b002f6f 100644 --- a/tests/integration/test_song_loader.gd +++ b/tests/integration/test_song_loader.gd @@ -15,7 +15,7 @@ func before_test() -> void: Playlist.reset_playlist() Playlist.is_song_downloading = false Playlist.number_of_tracks_to_preload = 1 - Settings.use_low_played_mode = false + Settings.random_mode = Settings.RandomMode.NORMAL _server = FakeHttpTransport.new().install() _server.respond_json("/music/list", []) _server.respond_bytes("/music/rand", FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) @@ -79,15 +79,22 @@ func test_fetch_song_keeps_going_until_the_cache_is_full() -> void: assert_int(_server.count("/music/rand")).is_equal(3) -func test_low_played_mode_uses_its_endpoint() -> void: - Settings.use_low_played_mode = true - _server.respond_bytes("/music/rand/low", - FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) +@warning_ignore("unused_parameter") +func test_each_random_mode_uses_its_endpoint(mode: Settings.RandomMode, path: String, + test_parameters: Array = [ + [Settings.RandomMode.NORMAL, "/music/rand"], + [Settings.RandomMode.LOW_PLAYED, "/music/rand/low"], + [Settings.RandomMode.CLASSIC, "/music/rand/classic"], + ]) -> void: + Settings.random_mode = mode + _server.respond_bytes(path, FileAccess.get_file_as_bytes("res://tests/data/short.mp3")) _loader.fetch_song() - assert_int(_server.count("/music/rand/low")).is_equal(1) - assert_int(_server.count("/music/rand")).is_equal(0) + assert_int(_server.count(path)).is_equal(1) + for other: String in ["/music/rand", "/music/rand/low", "/music/rand/classic"]: + if other != path: + assert_int(_server.count(other)).override_failure_message(other).is_equal(0) func test_fetch_song_if_idle_does_nothing_while_downloading() -> void: diff --git a/tests/unit/test_music_server_client.gd b/tests/unit/test_music_server_client.gd index 3e50432..09805db 100644 --- a/tests/unit/test_music_server_client.gd +++ b/tests/unit/test_music_server_client.gd @@ -59,16 +59,16 @@ func test_json_list_that_is_not_an_array_calls_on_failed() -> void: assert_array(outcome).contains_exactly(["failed"]) -func test_fetch_random_song_uses_normal_endpoint() -> void: - MusicServerClient.fetch_random_song(false, func(_body: PackedByteArray) -> void: pass) +@warning_ignore("unused_parameter") +func test_fetch_random_song_uses_the_endpoint_of_its_mode(mode: Settings.RandomMode, + path: String, test_parameters: Array = [ + [Settings.RandomMode.NORMAL, "/music/rand"], + [Settings.RandomMode.LOW_PLAYED, "/music/rand/low"], + [Settings.RandomMode.CLASSIC, "/music/rand/classic"], + ]) -> void: + MusicServerClient.fetch_random_song(mode, func(_body: PackedByteArray) -> void: pass) - assert_array(_server.requested_paths).contains_exactly(["/music/rand"]) - - -func test_fetch_random_song_uses_low_played_endpoint() -> void: - MusicServerClient.fetch_random_song(true, func(_body: PackedByteArray) -> void: pass) - - assert_array(_server.requested_paths).contains_exactly(["/music/rand/low"]) + assert_array(_server.requested_paths).contains_exactly([path]) func test_fetch_song_passes_raw_bytes() -> void: @@ -103,7 +103,7 @@ func test_failed_request_calls_on_failed() -> void: _server.respond_bytes("/music/rand", PackedByteArray(), 503) var outcome: Array[String] = [] - MusicServerClient.fetch_random_song(false, + MusicServerClient.fetch_random_song(Settings.RandomMode.NORMAL, func(_body: PackedByteArray) -> void: outcome.append("done"), func() -> void: outcome.append("failed")) @@ -370,8 +370,6 @@ func test_delete_token_without_token_sends_nothing() -> void: func test_endpoint_paths_and_methods() -> void: var noop: Callable = func(_data: Variant = null) -> void: pass var calls: Array[Array] = [ - [func() -> void: MusicServerClient.fetch_random_song_classic(noop), - "/music/rand/classic", HTTPClient.METHOD_GET], [func() -> void: MusicServerClient.fetch_next_song(noop), "/music/next", HTTPClient.METHOD_GET], [func() -> void: MusicServerClient.fetch_previous_song(noop), diff --git a/tests/unit/test_settings_files.gd b/tests/unit/test_settings_files.gd index c9c45f9..dc16f35 100644 --- a/tests/unit/test_settings_files.gd +++ b/tests/unit/test_settings_files.gd @@ -8,6 +8,7 @@ const SAVED_VALUES: Dictionary = { &"hide_ticks": true, &"winning_score": 13, &"inspiration_list_speed": 4, &"fullscreen": true, &"last_seen_version": "1.2.3", &"theme_id": "original_web", &"reduce_animations": true, + &"short_songs": true, &"short_song_seconds": 45, } var _snapshot: AutoloadSnapshot @@ -93,7 +94,7 @@ func test_statistics_settings_are_not_saved() -> void: var file: ConfigFile = _saved_file() assert_bool(file.has_section_key("quiz", "add_to_stats")).is_false() - assert_bool(file.has_section_key("quiz", "use_low_played_mode")).is_false() + assert_bool(file.has_section_key("quiz", "random_mode")).is_false() func test_queue_ids_are_saved_and_load_back() -> void: