Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0862d160e5 |
No files matched your search
+10
-5
@@ -24,15 +24,20 @@ static func enabled() -> bool:
|
||||
## one with the same key so they don't fight. Returns null when animations are off or the
|
||||
## node is not in the tree.
|
||||
static func tween(node: Node, key: StringName = &"anim") -> Tween:
|
||||
stop(node, key)
|
||||
if !enabled() || !node.is_inside_tree():
|
||||
return null
|
||||
var new_tween: Tween = node.create_tween()
|
||||
node.set_meta(StringName("_tween_" + key), new_tween)
|
||||
return new_tween
|
||||
|
||||
|
||||
## Stops the animation called [param key] on [param node], if one is running.
|
||||
static func stop(node: Node, key: StringName = &"anim") -> void:
|
||||
var meta: StringName = StringName("_tween_" + key)
|
||||
var old: Variant = node.get_meta(meta) if node.has_meta(meta) else null
|
||||
if old is Tween && (old as Tween).is_valid():
|
||||
(old as Tween).kill()
|
||||
if !enabled() || !node.is_inside_tree():
|
||||
return null
|
||||
var new_tween: Tween = node.create_tween()
|
||||
node.set_meta(meta, new_tween)
|
||||
return new_tween
|
||||
|
||||
|
||||
## Makes [param control] grow from its middle.
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
class_name AnimatedProgressBar
|
||||
extends ProgressBar
|
||||
## A progress bar with some life in it:
|
||||
## - [method wait] shows Godot's busy animation until there is any progress;
|
||||
## - [method move_to] glides to each new value instead of jumping;
|
||||
## - a soft highlight sweeps along the filled part, kept inside its rounded corners.
|
||||
##
|
||||
## The bar's style, e.g. [code]SyncProgressBar[/code], has a [code]shine[/code] colour for
|
||||
## the highlight, and two more styles go with it: [code]<style>Fill[/code], a Panel shaped
|
||||
## like the filled part that the highlight is kept inside, and [code]<style>Text[/code],
|
||||
## the Label with the percentage. With Reduce animations the bar only jumps to its values,
|
||||
## with no busy animation or highlight.
|
||||
|
||||
## How wide the highlight is, in pixels.
|
||||
const SHINE_WIDTH: float = 260.0
|
||||
## How fast the highlight moves, in pixels per second.
|
||||
const SHINE_SPEED: float = 520.0
|
||||
## How far past the end the highlight goes before starting over, so sweeps have a pause.
|
||||
const SHINE_GAP: float = 600.0
|
||||
|
||||
# The filled part's shape, which only its children are drawn inside
|
||||
var _mask: Panel
|
||||
var _shine: TextureRect
|
||||
# The percentage, drawn above the highlight rather than under it
|
||||
var _label: Label
|
||||
var _shine_x: float = -SHINE_WIDTH
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_mask = Panel.new()
|
||||
_mask.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mask.clip_children = CanvasItem.CLIP_CHILDREN_ONLY
|
||||
add_child(_mask)
|
||||
_shine = TextureRect.new()
|
||||
_shine.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_shine.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_shine.stretch_mode = TextureRect.STRETCH_SCALE
|
||||
_mask.add_child(_shine)
|
||||
_label = Label.new()
|
||||
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_label.visible = show_percentage
|
||||
add_child(_label)
|
||||
show_percentage = false
|
||||
_mask.theme_type_variation = StringName(String(theme_type_variation) + "Fill")
|
||||
_label.theme_type_variation = StringName(String(theme_type_variation) + "Text")
|
||||
theme_changed.connect(_update_style)
|
||||
value_changed.connect(func(_value: float) -> void: _update_label())
|
||||
_update_style()
|
||||
_update_label()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var fill_width: float = fill_width()
|
||||
_mask.size = Vector2(fill_width, size.y)
|
||||
_mask.visible = fill_width > 0.0 && !indeterminate && Anim.enabled()
|
||||
if !_mask.visible:
|
||||
return
|
||||
_shine_x += SHINE_SPEED * delta
|
||||
if _shine_x > fill_width + SHINE_GAP:
|
||||
_shine_x = -SHINE_WIDTH
|
||||
_shine.position = Vector2(_shine_x, 0.0)
|
||||
_shine.size = Vector2(SHINE_WIDTH, size.y)
|
||||
|
||||
|
||||
## Shows the busy animation, with no value, until [method move_to] has some progress.
|
||||
func wait() -> void:
|
||||
Anim.stop(self, &"progress")
|
||||
value = min_value
|
||||
indeterminate = Anim.enabled()
|
||||
_update_label()
|
||||
|
||||
|
||||
## Glides to [param target]. Any progress ends the busy animation.
|
||||
func move_to(target: float) -> void:
|
||||
if target > min_value:
|
||||
indeterminate = false
|
||||
var glide: Tween = Anim.tween(self, &"progress")
|
||||
if glide == null:
|
||||
value = target
|
||||
return
|
||||
glide.tween_property(self, "value", target, Anim.SLOW) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## How wide the filled part is, the way [ProgressBar] draws it.
|
||||
func fill_width() -> float:
|
||||
var margins: float = get_theme_stylebox(&"fill").get_minimum_size().x
|
||||
var filled: float = roundf(ratio_done() * (size.x - margins))
|
||||
return filled + margins if filled > 0.0 else 0.0
|
||||
|
||||
|
||||
func _update_style() -> void:
|
||||
var shine: Color = get_theme_color(&"shine")
|
||||
var clear: Color = shine
|
||||
clear.a = 0.0
|
||||
var gradient: Gradient = Gradient.new()
|
||||
gradient.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
||||
gradient.colors = PackedColorArray([clear, shine, clear])
|
||||
var texture: GradientTexture2D = GradientTexture2D.new()
|
||||
texture.gradient = gradient
|
||||
texture.width = 256
|
||||
texture.height = 1
|
||||
_shine.texture = texture
|
||||
|
||||
|
||||
func _update_label() -> void:
|
||||
if _label == null:
|
||||
return
|
||||
_label.text = "" if indeterminate else "%d%%" % roundi(ratio_done() * 100.0)
|
||||
|
||||
|
||||
## How much is done, from 0 to 1.
|
||||
func ratio_done() -> float:
|
||||
var span: float = max_value - min_value
|
||||
return clampf((value - min_value) / span, 0.0, 1.0) if span > 0.0 else 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxjx3kacfqg6j
|
||||
@@ -1,119 +0,0 @@
|
||||
class_name UploadFolder
|
||||
extends RefCounted
|
||||
## A folder picked or dropped to upload as a new soundtrack: its MP3s, ready to send, and
|
||||
## the other song files, which are left out because the server only takes MP3.
|
||||
##
|
||||
## The server wants one folder of songs, so songs in subfolders, such as disc folders,
|
||||
## are taken too, with the disc's number in front of their names: "Disc 1/01 Title.mp3"
|
||||
## is sent as "1-01 Title.mp3". With only one disc folder, its songs keep their names.
|
||||
## Hidden files and folders are left out, and names get rid of the characters the server
|
||||
## refuses.
|
||||
|
||||
## The only kind of song the server takes.
|
||||
const UPLOAD_EXTENSION: String = "mp3"
|
||||
## Other song files, listed as left out so the user knows.
|
||||
const OTHER_SONG_EXTENSIONS: Array[String] = [
|
||||
"ogg", "wav", "flac", "m4a", "aac", "opus", "wma", "ape", "aif", "aiff",
|
||||
]
|
||||
## Characters the server refuses in soundtrack and file names.
|
||||
const INVALID_NAME_CHARACTERS: String = "/\\:*?\"<>|"
|
||||
|
||||
## The folder.
|
||||
var path: String = ""
|
||||
## The soundtrack's name to suggest: the folder's name.
|
||||
var suggested_name: String = ""
|
||||
## The songs to upload, in order: [code]{"path": String, "name": String, "size": int}[/code],
|
||||
## with the name to send each one as.
|
||||
var songs: Array[Dictionary] = []
|
||||
## The song files left out, as paths in the folder, e.g. [code]"Disc 2/03 Song.flac"[/code].
|
||||
var skipped: Array[String] = []
|
||||
## The size of the songs to upload, in bytes.
|
||||
var total_size: int = 0
|
||||
|
||||
|
||||
## Reads the folder at [param folder].
|
||||
static func scan(folder: String) -> UploadFolder:
|
||||
var result: UploadFolder = UploadFolder.new()
|
||||
result.path = folder.trim_suffix("/").trim_suffix("\\")
|
||||
result.suggested_name = clean_name(result.path.get_file())
|
||||
result._add_files(result.path, "", "")
|
||||
var discs: Array[String] = []
|
||||
for disc: String in _visible(DirAccess.get_directories_at(result.path)):
|
||||
if _has_songs(result.path.path_join(disc)):
|
||||
discs.append(disc)
|
||||
for disc: int in discs.size():
|
||||
result._add_folder(result.path.path_join(discs[disc]), discs[disc],
|
||||
"%d-" % (disc + 1) if discs.size() > 1 else "")
|
||||
return result
|
||||
|
||||
|
||||
## [param name] without the characters the server refuses, a leading dot, or a trailing
|
||||
## dot or space.
|
||||
static func clean_name(name: String) -> String:
|
||||
var cleaned: String = ""
|
||||
for character: String in name:
|
||||
var control: bool = character.unicode_at(0) < 32 || character.unicode_at(0) == 127
|
||||
cleaned += "-" if INVALID_NAME_CHARACTERS.contains(character) || control \
|
||||
else character
|
||||
cleaned = cleaned.strip_edges().lstrip(".")
|
||||
while cleaned.ends_with(".") || cleaned.ends_with(" "):
|
||||
cleaned = cleaned.left(-1)
|
||||
return cleaned
|
||||
|
||||
|
||||
## Whether there is anything to upload.
|
||||
func has_songs() -> bool:
|
||||
return !songs.is_empty()
|
||||
|
||||
|
||||
# Adds the files of [param folder] and of the folders in it, all with [param prefix]
|
||||
func _add_folder(folder: String, relative: String, prefix: String) -> void:
|
||||
_add_files(folder, relative, prefix)
|
||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
||||
_add_folder(folder.path_join(inner), relative.path_join(inner), prefix)
|
||||
|
||||
|
||||
func _add_files(folder: String, relative: String, prefix: String) -> void:
|
||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
||||
var extension: String = file_name.get_extension().to_lower()
|
||||
if extension == UPLOAD_EXTENSION:
|
||||
var file_path: String = folder.path_join(file_name)
|
||||
var size: int = FileAccess.get_size(file_path)
|
||||
songs.append({"path": file_path, "name": _free_name(prefix + clean_name(file_name)),
|
||||
"size": size})
|
||||
total_size += size
|
||||
elif extension in OTHER_SONG_EXTENSIONS:
|
||||
skipped.append(relative.path_join(file_name) if !relative.is_empty() else file_name)
|
||||
|
||||
|
||||
# [param name], or with " (2)" and so on before its extension when a song already has it
|
||||
func _free_name(name: String) -> String:
|
||||
var taken: Array = songs.map(func(song: Dictionary) -> String: return song.name.to_lower())
|
||||
var candidate: String = name
|
||||
var number: int = 2
|
||||
while taken.has(candidate.to_lower()):
|
||||
candidate = "%s (%d).%s" % [name.get_basename(), number, name.get_extension()]
|
||||
number += 1
|
||||
return candidate
|
||||
|
||||
|
||||
# Whether [param folder] or a folder in it has a song, to upload or to list as left out
|
||||
static func _has_songs(folder: String) -> bool:
|
||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
||||
var extension: String = file_name.get_extension().to_lower()
|
||||
if extension == UPLOAD_EXTENSION || extension in OTHER_SONG_EXTENSIONS:
|
||||
return true
|
||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
||||
if _has_songs(folder.path_join(inner)):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# The names that are not hidden, in natural order, so "2" comes before "10"
|
||||
static func _visible(names: PackedStringArray) -> Array[String]:
|
||||
var shown: Array[String] = []
|
||||
for name: String in names:
|
||||
if !name.begins_with("."):
|
||||
shown.append(name)
|
||||
shown.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
||||
return shown
|
||||
@@ -1 +0,0 @@
|
||||
uid://3amk1cte2idb
|
||||
@@ -13,7 +13,6 @@
|
||||
[ext_resource type="PackedScene" path="res://popups/SoundtrackStatsPopup.tscn" id="soundtrack_stats_popup_scene"]
|
||||
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://popups/SettingsPopup.tscn" id="settings_popup_scene"]
|
||||
[ext_resource type="PackedScene" path="res://popups/UpdatePopup.tscn" id="update_popup_scene"]
|
||||
[ext_resource type="PackedScene" path="res://popups/UploadPopup.tscn" id="upload_popup_scene"]
|
||||
|
||||
[node name="MainWindow" type="Control" unique_id=407149696]
|
||||
layout_mode = 3
|
||||
@@ -120,12 +119,3 @@ offset_left = 640.0
|
||||
offset_top = 390.0
|
||||
offset_right = 640.0
|
||||
offset_bottom = 390.0
|
||||
|
||||
[node name="UploadPopup" parent="." instance=ExtResource("upload_popup_scene")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 580.0
|
||||
offset_top = 200.0
|
||||
offset_right = 580.0
|
||||
offset_bottom = 200.0
|
||||
+1
-28
@@ -21,7 +21,6 @@ var popup_backdrop: PopupBackdrop
|
||||
@onready var log_popup: LogPopup = %LogPopup
|
||||
@onready var settings_popup: SettingsPopup = %SettingsPopup
|
||||
@onready var update_popup: UpdatePopup = %UpdatePopup
|
||||
@onready var upload_popup: UploadPopup = %UploadPopup
|
||||
@onready var statistics_view: StatisticsView = $StatisticsView
|
||||
@onready var soundtrack_stats_popup: SoundtrackStatsPopup = $SoundtrackStatsPopup
|
||||
|
||||
@@ -41,9 +40,6 @@ func _ready() -> void:
|
||||
top_bar.settings_pressed.connect(settings_popup.open)
|
||||
top_bar.update_pressed.connect(update_popup.show_update)
|
||||
update_popup.restarting.connect(quiz.save_now)
|
||||
statistics_view.add_soundtrack_pressed.connect(upload_popup.choose_folder)
|
||||
upload_popup.soundtrack_added.connect(_on_soundtrack_added)
|
||||
get_window().files_dropped.connect(on_files_dropped)
|
||||
settings_popup.server_changed.connect(quiz.on_server_changed)
|
||||
settings_popup.characters_changed.connect(quiz.character_select_popup.load_characters)
|
||||
settings_popup.clear_cache_requested.connect(clear_cache)
|
||||
@@ -82,29 +78,6 @@ func clear_cache() -> void:
|
||||
quiz.character_select_popup.load_characters()
|
||||
|
||||
|
||||
## Offers to upload a folder dropped on the window as a new soundtrack. Dropped songs
|
||||
## stand for the folder they are in.
|
||||
func on_files_dropped(paths: PackedStringArray) -> void:
|
||||
if paths.is_empty():
|
||||
return
|
||||
var folder: String = paths[0]
|
||||
for path: String in paths:
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
folder = path
|
||||
break
|
||||
if !DirAccess.dir_exists_absolute(folder):
|
||||
folder = folder.get_base_dir()
|
||||
upload_popup.show_folder(folder)
|
||||
|
||||
|
||||
# A new soundtrack is found by search, listed for inspiration and in the statistics
|
||||
func _on_soundtrack_added(_soundtrack: SoundtrackWithSongList) -> void:
|
||||
search_popup.get_list_of_soundtracks()
|
||||
quiz.inspiration_list.load_soundtracks()
|
||||
if statistics_view.visible:
|
||||
statistics_view.refresh()
|
||||
|
||||
|
||||
# The quiz finds a soundtrack's cover by the cover times in the soundtrack list
|
||||
func _remember_cover_times(soundtracks: Array[Soundtrack]) -> void:
|
||||
var times: Dictionary = {}
|
||||
@@ -143,7 +116,7 @@ func _input(event: InputEvent) -> void:
|
||||
|
||||
func _popups() -> Array[BasePopup]:
|
||||
return [search_popup, sync_popup, qr_popup, shortcuts_popup, about_popup, log_popup,
|
||||
soundtrack_stats_popup, settings_popup, update_popup, upload_popup]
|
||||
soundtrack_stats_popup, settings_popup, update_popup]
|
||||
|
||||
|
||||
# How many songs to download ahead can be changed in the settings. Waiting until the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://dpdem7pdxweb5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c3ejukwth512a" path="res://popups/sync_popup.gd" id="1_8ba47"]
|
||||
[ext_resource type="Script" path="res://common/animated_progress_bar.gd" id="progress_bar_script"]
|
||||
|
||||
[node name="SyncPopup" type="Control" unique_id=1302914356 node_paths=PackedStringArray("panel")]
|
||||
layout_mode = 3
|
||||
@@ -62,11 +63,11 @@ layout_mode = 2
|
||||
|
||||
[node name="SyncProgressBar" type="ProgressBar" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/ProgressHBoxContainer" unique_id=719135231]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1300, 0)
|
||||
custom_minimum_size = Vector2(1300, 32)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 6
|
||||
step = 1.0
|
||||
rounded = true
|
||||
theme_type_variation = &"SyncProgressBar"
|
||||
script = ExtResource("progress_bar_script")
|
||||
|
||||
[node name="SyncHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1156646398]
|
||||
unique_name_in_owner = true
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://popups/upload_popup.gd" id="popup_script"]
|
||||
|
||||
[node name="UploadPopup" type="Control" node_paths=PackedStringArray("panel")]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("popup_script")
|
||||
panel = NodePath("UploadPanel")
|
||||
|
||||
[node name="UploadPanel" type="Panel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 760.0
|
||||
offset_bottom = 680.0
|
||||
theme_type_variation = &"PopupBackground"
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="UploadPanel"]
|
||||
layout_mode = 0
|
||||
offset_left = 24.0
|
||||
offset_top = 16.0
|
||||
offset_right = 736.0
|
||||
offset_bottom = 664.0
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="UploadPanel/Rows"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"Title"
|
||||
text = "Add a soundtrack"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HelpLabel" type="Label" parent="UploadPanel/Rows"]
|
||||
layout_mode = 2
|
||||
autowrap_mode = 3
|
||||
text = "The songs are uploaded to the server, which adds them as a new soundtrack right away. Songs in disc folders are added too, with the disc's number first."
|
||||
|
||||
[node name="NameRow" type="HBoxContainer" parent="UploadPanel/Rows"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="NameTitle" type="Label" parent="UploadPanel/Rows/NameRow"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"SettingsRowLabel"
|
||||
text = "Name"
|
||||
|
||||
[node name="NameField" type="LineEdit" parent="UploadPanel/Rows/NameRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Soundtrack name"
|
||||
max_length = 200
|
||||
|
||||
[node name="ChooseFolderButton" type="Button" parent="UploadPanel/Rows/NameRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
text = "Other folder…"
|
||||
|
||||
[node name="FolderLabel" type="Label" parent="UploadPanel/Rows"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"TextMedium"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="SongScroll" type="ScrollContainer" parent="UploadPanel/Rows"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="SongList" type="VBoxContainer" parent="UploadPanel/Rows/SongScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SummaryLabel" type="Label" parent="UploadPanel/Rows"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="UploadProgress" type="ProgressBar" parent="UploadPanel/Rows"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
show_percentage = false
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UploadPanel/Rows"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="UploadPanel/Rows"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 2
|
||||
|
||||
[node name="CancelButton" type="Button" parent="UploadPanel/Rows/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
text = "Cancel"
|
||||
|
||||
[node name="UploadButton" type="Button" parent="UploadPanel/Rows/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
text = "Upload"
|
||||
|
||||
[node name="FolderDialog" type="FileDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "Choose a soundtrack folder"
|
||||
initial_position = 2
|
||||
size = Vector2i(800, 500)
|
||||
ok_button_text = "Select Current Folder"
|
||||
file_mode = 2
|
||||
access = 2
|
||||
use_native_dialog = true
|
||||
+15
-3
@@ -17,7 +17,7 @@ var has_synced: bool = false
|
||||
@onready var no_changes_label: Label = %NoChangesLabel
|
||||
@onready var progress_hbox: HBoxContainer = %ProgressHBoxContainer
|
||||
@onready var sync_hbox: HBoxContainer = %SyncHBoxContainer
|
||||
@onready var sync_progress_bar: ProgressBar = %SyncProgressBar
|
||||
@onready var sync_progress_bar: AnimatedProgressBar = %SyncProgressBar
|
||||
@onready var soundtracks_added_vbox: VBoxContainer = %SoundtracksAddedVBox
|
||||
@onready var soundtracks_added_separator: VSeparator = %SoundtracksAddedSeparator
|
||||
@onready var soundtracks_readded_vbox: VBoxContainer = %SoundtracksReAddedVBox
|
||||
@@ -77,13 +77,24 @@ func _on_sync_timeout() -> void:
|
||||
GameLog.debug("data_received: ", data_received)
|
||||
if data_received != null:
|
||||
if data_received.has("progress"):
|
||||
sync_progress_bar.value = int(data_received.progress)
|
||||
sync_progress_bar.move_to(int(data_received.progress))
|
||||
sync_time.text = data_received.time_spent if data_received.has("time_spent") else ""
|
||||
else:
|
||||
on_sync_finished(data_received)
|
||||
_finish_progress(data_received)
|
||||
MusicServerClient.fetch_sync_progress(get_progress)
|
||||
|
||||
|
||||
# Fills the bar and lets it pop before the result takes its place
|
||||
func _finish_progress(data_received: Dictionary) -> void:
|
||||
sync_timer.stop()
|
||||
if Anim.enabled() && progress_hbox.visible:
|
||||
sync_progress_bar.move_to(sync_progress_bar.max_value)
|
||||
await get_tree().create_timer(Anim.SLOW).timeout
|
||||
Anim.pop(sync_progress_bar, 1.04, Anim.SLOW)
|
||||
await get_tree().create_timer(Anim.SLOW).timeout
|
||||
on_sync_finished(data_received)
|
||||
|
||||
|
||||
func on_sync_finished(data_received: Dictionary) -> void:
|
||||
has_synced = true
|
||||
reset_sync_result()
|
||||
@@ -218,6 +229,7 @@ func show_sync_result() -> void:
|
||||
func show_sync_progress() -> void:
|
||||
sync_title_label.text = "Sync in Progress"
|
||||
progress_hbox.visible = true
|
||||
sync_progress_bar.wait()
|
||||
no_changes_label.visible = false
|
||||
sync_hbox.visible = false
|
||||
caught_errors_vbox.visible = false
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
class_name UploadPopup
|
||||
extends BasePopup
|
||||
## Adds a new soundtrack to the server from a folder: picked with Add soundtrack… in the
|
||||
## statistics, or dropped on the window.
|
||||
##
|
||||
## Lists the folder's songs (see [UploadFolder]), lets the name be changed, and uploads
|
||||
## the songs one at a time with a progress bar. A song that fails is sent again a couple
|
||||
## of times before the upload is cancelled. The server then adds the soundtrack at once,
|
||||
## without a sync.
|
||||
|
||||
## Emitted when the server has added the soundtrack.
|
||||
signal soundtrack_added(soundtrack: SoundtrackWithSongList)
|
||||
|
||||
## How many times a song that failed is sent again before the upload is given up.
|
||||
const RETRIES: int = 2
|
||||
|
||||
## The folder shown.
|
||||
var folder: UploadFolder
|
||||
|
||||
# The upload on the server, while there is one
|
||||
var _upload_id: String = ""
|
||||
# The song being sent, and how often it failed
|
||||
var _next: int = 0
|
||||
var _failures: int = 0
|
||||
var _uploading: bool = false
|
||||
|
||||
@onready var name_field: LineEdit = %NameField
|
||||
@onready var choose_folder_button: Button = %ChooseFolderButton
|
||||
@onready var folder_label: Label = %FolderLabel
|
||||
@onready var song_list: VBoxContainer = %SongList
|
||||
@onready var summary_label: Label = %SummaryLabel
|
||||
@onready var upload_progress: ProgressBar = %UploadProgress
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
@onready var cancel_button: Button = %CancelButton
|
||||
@onready var upload_button: Button = %UploadButton
|
||||
@onready var folder_dialog: FileDialog = %FolderDialog
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
choose_folder_button.pressed.connect(folder_dialog.popup_centered)
|
||||
folder_dialog.dir_selected.connect(show_folder)
|
||||
cancel_button.pressed.connect(close)
|
||||
upload_button.pressed.connect(upload)
|
||||
name_field.text_changed.connect(func(_text: String) -> void: _update_buttons())
|
||||
name_field.text_submitted.connect(func(_text: String) -> void: upload())
|
||||
|
||||
|
||||
## Asks for a folder, then shows it.
|
||||
func choose_folder() -> void:
|
||||
folder_dialog.popup_centered()
|
||||
|
||||
|
||||
## Lists the songs in the folder at [param path] and opens the popup.
|
||||
func show_folder(path: String) -> void:
|
||||
folder = UploadFolder.scan(path)
|
||||
name_field.text = folder.suggested_name
|
||||
folder_label.text = "From " + short_path(folder.path)
|
||||
folder_label.tooltip_text = folder.path
|
||||
_fill_song_list()
|
||||
summary_label.text = summary_text(folder)
|
||||
upload_progress.visible = false
|
||||
status_label.visible = false
|
||||
upload_button.visible = true
|
||||
cancel_button.text = "Cancel"
|
||||
if Settings.is_offline():
|
||||
_show_status("Adding a soundtrack needs the server, not a music folder.")
|
||||
elif !folder.has_songs():
|
||||
_show_status("There are no MP3 files in %s." % folder.path.get_file())
|
||||
_set_uploading(false)
|
||||
open()
|
||||
|
||||
|
||||
## Starts uploading the songs as a soundtrack with the name in the name field.
|
||||
func upload() -> void:
|
||||
if upload_button.disabled || !upload_button.visible:
|
||||
return
|
||||
if Settings.currently_syncing:
|
||||
_show_status("The server is syncing. Try again when it is done.")
|
||||
return
|
||||
_set_uploading(true)
|
||||
upload_progress.max_value = folder.songs.size()
|
||||
upload_progress.value = 0
|
||||
upload_progress.visible = true
|
||||
_show_status("Starting the upload…")
|
||||
MusicServerClient.start_upload(UploadFolder.clean_name(name_field.text), _on_started,
|
||||
func() -> void: _stop("Could not start the upload"))
|
||||
|
||||
|
||||
## Stops an upload under way, and closes the popup.
|
||||
func close() -> void:
|
||||
if _uploading:
|
||||
_uploading = false
|
||||
_cancel_on_server()
|
||||
super()
|
||||
|
||||
|
||||
## The last two parts of [param path], e.g. "…/Soundtracks/Chrono Trigger", since the
|
||||
## start of a long path says the least.
|
||||
static func short_path(path: String) -> String:
|
||||
var parts: PackedStringArray = path.replace("\\", "/").split("/", false)
|
||||
if parts.size() <= 2:
|
||||
return path
|
||||
return "…/" + "/".join(parts.slice(-2))
|
||||
|
||||
|
||||
## "30 songs, 245.1 MB", and how many files are left out.
|
||||
static func summary_text(shown: UploadFolder) -> String:
|
||||
var text: String = "%d %s, %s" % [shown.songs.size(),
|
||||
"song" if shown.songs.size() == 1 else "songs",
|
||||
SettingsPopup.size_text(shown.total_size)]
|
||||
if !shown.skipped.is_empty():
|
||||
text += ". %d %s left out: only MP3 files can be uploaded." % [shown.skipped.size(),
|
||||
"file" if shown.skipped.size() == 1 else "files"]
|
||||
return text
|
||||
|
||||
|
||||
func _on_started(upload_info: Upload) -> void:
|
||||
if !_uploading:
|
||||
# Closed while the server was answering
|
||||
MusicServerClient.cancel_upload(upload_info.id)
|
||||
return
|
||||
_upload_id = upload_info.id
|
||||
_next = 0
|
||||
_failures = 0
|
||||
_send_next()
|
||||
|
||||
|
||||
func _send_next() -> void:
|
||||
if !_uploading:
|
||||
return
|
||||
if _next >= folder.songs.size():
|
||||
_show_status("Adding the soundtrack…")
|
||||
MusicServerClient.finish_upload(_upload_id, _on_finished,
|
||||
func() -> void: _stop("Could not add the soundtrack"))
|
||||
return
|
||||
var song: Dictionary = folder.songs[_next]
|
||||
_show_status("Uploading %d of %d: %s" % [_next + 1, folder.songs.size(), song.name])
|
||||
# One song at a time, so only one is held in memory
|
||||
var bytes: PackedByteArray = FileAccess.get_file_as_bytes(song.path)
|
||||
if bytes.is_empty():
|
||||
MusicServerClient.last_error = "the file could not be read"
|
||||
_stop("Could not upload " + song.name)
|
||||
return
|
||||
MusicServerClient.upload_song(_upload_id, song.name, bytes, _on_song_sent, _on_song_failed)
|
||||
|
||||
|
||||
func _on_song_sent() -> void:
|
||||
_next += 1
|
||||
_failures = 0
|
||||
upload_progress.value = _next
|
||||
_send_next()
|
||||
|
||||
|
||||
func _on_song_failed() -> void:
|
||||
_failures += 1
|
||||
if _failures <= RETRIES && _uploading:
|
||||
_send_next()
|
||||
return
|
||||
_stop("Could not upload " + folder.songs[_next].name)
|
||||
|
||||
|
||||
func _on_finished(soundtrack: SoundtrackWithSongList) -> void:
|
||||
_upload_id = ""
|
||||
_uploading = false
|
||||
upload_progress.value = upload_progress.max_value
|
||||
_show_status("Added %s with %d %s." % [soundtrack.name, soundtrack.songs.size(),
|
||||
"song" if soundtrack.songs.size() == 1 else "songs"])
|
||||
upload_button.visible = false
|
||||
cancel_button.text = "Close"
|
||||
_set_uploading(false)
|
||||
soundtrack_added.emit(soundtrack)
|
||||
|
||||
|
||||
# Gives up, says why with the server's reason, and removes what was uploaded
|
||||
func _stop(what: String) -> void:
|
||||
var reason: String = MusicServerClient.last_error
|
||||
_cancel_on_server()
|
||||
_uploading = false
|
||||
upload_progress.visible = false
|
||||
_set_uploading(false)
|
||||
_show_status("%s: %s." % [what,
|
||||
reason if !reason.is_empty() else "the server could not be reached"])
|
||||
|
||||
|
||||
func _cancel_on_server() -> void:
|
||||
if !_upload_id.is_empty():
|
||||
MusicServerClient.cancel_upload(_upload_id)
|
||||
_upload_id = ""
|
||||
|
||||
|
||||
func _set_uploading(uploading: bool) -> void:
|
||||
_uploading = uploading
|
||||
name_field.editable = !uploading
|
||||
choose_folder_button.disabled = uploading
|
||||
_update_buttons()
|
||||
|
||||
|
||||
func _update_buttons() -> void:
|
||||
upload_button.disabled = _uploading || folder == null || !folder.has_songs() \
|
||||
|| UploadFolder.clean_name(name_field.text).is_empty() || Settings.is_offline()
|
||||
|
||||
|
||||
func _fill_song_list() -> void:
|
||||
Settings.delete_children(song_list)
|
||||
var row: int = 0
|
||||
for song: Dictionary in folder.songs:
|
||||
song_list.add_child(ListRow.create("%s · %s" % [song.name,
|
||||
SettingsPopup.size_text(song.size)], row, &"TextMedium"))
|
||||
row += 1
|
||||
for skipped: String in folder.skipped:
|
||||
var list_row: ListRow = ListRow.create("%s · not MP3, left out" % skipped, row,
|
||||
&"TextMedium")
|
||||
list_row.label.self_modulate.a = 0.5
|
||||
song_list.add_child(list_row)
|
||||
row += 1
|
||||
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
status_label.text = message
|
||||
status_label.visible = true
|
||||
@@ -1 +0,0 @@
|
||||
uid://utn0m7614g61
|
||||
@@ -5,6 +5,7 @@ extends RefCounted
|
||||
## The [code]ProgressResponse[/code] schema of the MusicServer API.
|
||||
|
||||
var progress: String
|
||||
## How long the sync has been running, as minutes and seconds, e.g. "02:15"
|
||||
var time_spent: String
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ var soundtracks_changed_content: Array[String]
|
||||
var soundtracks_changed_title: Dictionary[String, String]
|
||||
var soundtracks_re_added: Array[String]
|
||||
var soundtracks_removed: Array[String]
|
||||
## How long the sync took, as minutes and seconds, e.g. "02:15"
|
||||
var total_time: String
|
||||
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by tools/generate_api_types.gd from server/openapi.json.
|
||||
# Do not edit; change the server and generate again.
|
||||
class_name Upload
|
||||
extends RefCounted
|
||||
## The [code]Upload[/code] schema of the MusicServer API.
|
||||
|
||||
## The songs received so far, sorted by name
|
||||
var files: Array[UploadFile]
|
||||
var id: String
|
||||
## Name of the new soundtrack, also its folder name
|
||||
var name: String
|
||||
var started: String
|
||||
## Size of all songs received so far, in bytes
|
||||
var total_size: int
|
||||
|
||||
|
||||
## Reads the fields from parsed JSON. Missing fields keep their defaults.
|
||||
static func from_dict(data: Dictionary) -> Upload:
|
||||
var result: Upload = Upload.new()
|
||||
for item: Variant in ApiJson.get_array(data, "files"):
|
||||
if item is Dictionary:
|
||||
result.files.append(UploadFile.from_dict(item))
|
||||
result.id = ApiJson.get_string(data, "id", "")
|
||||
result.name = ApiJson.get_string(data, "name", "")
|
||||
result.started = ApiJson.get_string(data, "started", "")
|
||||
result.total_size = ApiJson.get_int(data, "total_size", 0)
|
||||
return result
|
||||
|
||||
|
||||
## Returns the fields under their JSON names, ready for [method JSON.stringify].
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"files": files.map(func(item: UploadFile) -> Dictionary: return item.to_dict()),
|
||||
"id": id,
|
||||
"name": name,
|
||||
"started": started,
|
||||
"total_size": total_size,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://diemnuinawufa
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by tools/generate_api_types.gd from server/openapi.json.
|
||||
# Do not edit; change the server and generate again.
|
||||
class_name UploadFile
|
||||
extends RefCounted
|
||||
## The [code]UploadFile[/code] schema of the MusicServer API.
|
||||
|
||||
var name: String
|
||||
var size: int
|
||||
|
||||
|
||||
## Reads the fields from parsed JSON. Missing fields keep their defaults.
|
||||
static func from_dict(data: Dictionary) -> UploadFile:
|
||||
var result: UploadFile = UploadFile.new()
|
||||
result.name = ApiJson.get_string(data, "name", "")
|
||||
result.size = ApiJson.get_int(data, "size", 0)
|
||||
return result
|
||||
|
||||
|
||||
## Returns the fields under their JSON names, ready for [method JSON.stringify].
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
"size": size,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://c5a5ig8cy44hy
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by tools/generate_api_types.gd from server/openapi.json.
|
||||
# Do not edit; change the server and generate again.
|
||||
class_name UploadRequest
|
||||
extends RefCounted
|
||||
## The [code]UploadRequest[/code] schema of the MusicServer API.
|
||||
|
||||
## Name of the new soundtrack, also its folder name in the music folder
|
||||
var name: String
|
||||
|
||||
|
||||
## Reads the fields from parsed JSON. Missing fields keep their defaults.
|
||||
static func from_dict(data: Dictionary) -> UploadRequest:
|
||||
var result: UploadRequest = UploadRequest.new()
|
||||
result.name = ApiJson.get_string(data, "name", "")
|
||||
return result
|
||||
|
||||
|
||||
## Returns the fields under their JSON names, ready for [method JSON.stringify].
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"name": name,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://bmqrxhvdnss3j
|
||||
@@ -30,8 +30,6 @@ const CLIENT_TYPE: String = "godot"
|
||||
const QUEUE_HEADER: String = "X-Queue-Id"
|
||||
## The type of the JSON bodies sent.
|
||||
const JSON_TYPE: String = "application/json"
|
||||
## The type of uploaded songs.
|
||||
const MP3_TYPE: String = "audio/mpeg"
|
||||
## The files of a MusicPlayer release that can be downloaded: the Windows game, the Linux
|
||||
## game, and the Linux package with an installer.
|
||||
const DOWNLOAD_ASSETS: Array[String] = ["windows", "linux", "linux-package"]
|
||||
@@ -181,43 +179,6 @@ func delete_soundtrack_cover(
|
||||
on_failed)
|
||||
|
||||
|
||||
## Starts uploading a new soundtrack called [param name], which is also its folder name on
|
||||
## the server, and calls [param on_done] with the [Upload]. Refused when the name is taken
|
||||
## or not a valid folder name, or while the server syncs; see [member last_error].
|
||||
func start_upload(name: String, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
var request: UploadRequest = UploadRequest.new()
|
||||
request.name = name
|
||||
var path: String = "/soundtracks/uploads"
|
||||
_request(path, HTTPClient.METHOD_POST,
|
||||
_parse_json.bind(path, _as_object.bind(Upload, on_done, on_failed), on_failed),
|
||||
on_failed, true, JSON.stringify(request.to_dict()).to_utf8_buffer())
|
||||
|
||||
|
||||
## Sends the MP3 [param song] as [param file_name] in the upload [param upload_id]. A song
|
||||
## of the same name is replaced, so a failed song can be sent again.
|
||||
func upload_song(upload_id: String, file_name: String, song: PackedByteArray,
|
||||
on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
_request(_upload_path(upload_id) + "/files/" + file_name.uri_encode(),
|
||||
HTTPClient.METHOD_PUT, _call_ignoring_body.bind(on_done), on_failed, true, song,
|
||||
MP3_TYPE)
|
||||
|
||||
|
||||
## Adds the uploaded songs to the server's library as a new soundtrack and calls
|
||||
## [param on_done] with it as a [SoundtrackWithSongList].
|
||||
func finish_upload(upload_id: String, on_done: Callable,
|
||||
on_failed: Callable = Callable()) -> void:
|
||||
var path: String = _upload_path(upload_id) + "/finish"
|
||||
_request(path, HTTPClient.METHOD_POST, _parse_json.bind(path,
|
||||
_as_object.bind(SoundtrackWithSongList, on_done, on_failed), on_failed), on_failed)
|
||||
|
||||
|
||||
## Deletes the unfinished upload [param upload_id] and its songs.
|
||||
func cancel_upload(upload_id: String, on_done: Callable = Callable(),
|
||||
on_failed: Callable = Callable()) -> void:
|
||||
_request(_upload_path(upload_id), HTTPClient.METHOD_DELETE,
|
||||
_call_ignoring_body.bind(on_done), on_failed)
|
||||
|
||||
|
||||
## Replaces the alternative names of the soundtrack with [param uuid] with
|
||||
## [param names]. The server trims them, drops duplicates and the soundtrack's own name,
|
||||
## and calls [param on_done] with the updated [Soundtrack].
|
||||
@@ -676,20 +637,6 @@ static func header_value(headers: PackedStringArray, header_name: String) -> Str
|
||||
return ""
|
||||
|
||||
|
||||
static func _upload_path(upload_id: String) -> String:
|
||||
return "/soundtracks/uploads/" + upload_id.uri_encode()
|
||||
|
||||
|
||||
# Calls [param on_done] with [param data] read as [param type], or [param on_failed] when
|
||||
# it is not a JSON object
|
||||
static func _as_object(data: Variant, type: GDScript, on_done: Callable,
|
||||
on_failed: Callable) -> void:
|
||||
if !(data is Dictionary):
|
||||
_call_if_valid(on_failed)
|
||||
return
|
||||
on_done.call(type.from_dict(data))
|
||||
|
||||
|
||||
static func _cover_path(uuid: String) -> String:
|
||||
return "/soundtracks/%s/cover" % uuid.uri_encode()
|
||||
|
||||
|
||||
+4352
-1
File diff suppressed because it is too large.
Load diff
@@ -107,17 +107,6 @@ popup/item_2/id = 2
|
||||
popup/item_3/text = "Least played"
|
||||
popup/item_3/id = 3
|
||||
|
||||
[node name="AddSeparator" type="VSeparator" parent="Toolbar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AddSoundtrackButton" type="Button" parent="Toolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Upload a folder of songs to the server as a new soundtrack.
|
||||
You can also drop a folder anywhere on the window."
|
||||
focus_mode = 0
|
||||
text = "Add soundtrack…"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=1212000010]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
|
||||
@@ -11,8 +11,6 @@ extends Panel
|
||||
|
||||
## Emitted when a card is clicked, to show all songs of [param soundtrack].
|
||||
signal soundtrack_selected(soundtrack: SoundtrackWithSongs)
|
||||
## Emitted when Add soundtrack… is pressed.
|
||||
signal add_soundtrack_pressed
|
||||
|
||||
## The orders the cards can be sorted in, in the order of the sort menu.
|
||||
enum SortOrder { A_TO_Z, Z_TO_A, MOST_PLAYED, LEAST_PLAYED }
|
||||
@@ -59,7 +57,6 @@ var _songs_ready: bool = false
|
||||
@onready var soundtracks_mode_button: Button = %SoundtracksModeButton
|
||||
@onready var songs_mode_button: Button = %SongsModeButton
|
||||
@onready var search_delay: Timer = %SearchDelay
|
||||
@onready var add_soundtrack_button: Button = %AddSoundtrackButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -72,7 +69,6 @@ func _ready() -> void:
|
||||
show_option.item_selected.connect(func(_index: int) -> void: apply_filter())
|
||||
sort_option.item_selected.connect(func(_index: int) -> void: apply_sort())
|
||||
retry_button.pressed.connect(refresh)
|
||||
add_soundtrack_button.pressed.connect(add_soundtrack_pressed.emit)
|
||||
retry_button.visible = false
|
||||
# The cards have moved by the end of the frame
|
||||
card_scroll.get_v_scroll_bar().value_changed.connect(
|
||||
|
||||
+322
-2297
File diff suppressed because it is too large.
Load diff
@@ -43,7 +43,6 @@ var queue_id: String = ""
|
||||
|
||||
var _responses: Dictionary = {}
|
||||
var _successes_left: Dictionary = {}
|
||||
var _failures_left: Dictionary = {}
|
||||
var _held_paths: Dictionary = {}
|
||||
var _previous_transport: HttpTransport
|
||||
var _previous_queue_ids: Dictionary[String, String]
|
||||
@@ -90,11 +89,6 @@ func respond_bytes(path: String, body: PackedByteArray, response_code: int = 200
|
||||
_successes_left.erase(path)
|
||||
|
||||
|
||||
## Answers the next [param failures] requests to [param path] with a 503, then as usual.
|
||||
func fail_first(path: String, failures: int) -> void:
|
||||
_failures_left[path] = failures
|
||||
|
||||
|
||||
## Lets [param successes] more requests to [param path] succeed, then answers 503 until
|
||||
## a new response is registered for the path.
|
||||
func fail_after(path: String, successes: int) -> void:
|
||||
@@ -162,10 +156,6 @@ func _answer(path: String, on_complete: Callable, follow_redirects: bool = true)
|
||||
var headers: PackedStringArray = []
|
||||
if !queue_id.is_empty() && path != "/token":
|
||||
headers.append("X-Queue-Id: " + queue_id)
|
||||
if _failures_left.get(path, 0) > 0:
|
||||
_failures_left[path] -= 1
|
||||
on_complete.call(503, PackedByteArray(), headers)
|
||||
return
|
||||
if _successes_left.has(path):
|
||||
if _successes_left[path] == 0:
|
||||
on_complete.call(503, PackedByteArray(), headers)
|
||||
|
||||
+2
-37
@@ -18,7 +18,7 @@ const SONG_INFO: Dictionary = {
|
||||
"SongNo": 0, "Soundtrack": "Chrono Trigger", "SoundtrackUuid": "soundtrack-0",
|
||||
"Song": "Corridors of Time",
|
||||
}
|
||||
const SYNC_PROGRESS: Dictionary = {"progress": 40, "time_spent": "00:00:02"}
|
||||
const SYNC_PROGRESS: Dictionary = {"progress": 40, "time_spent": "00:02"}
|
||||
const SYNC_RESULT: Dictionary = {
|
||||
"soundtracks_added": ["Terranigma", "Lufia II"],
|
||||
"soundtracks_re_added": [],
|
||||
@@ -26,7 +26,7 @@ const SYNC_RESULT: Dictionary = {
|
||||
"soundtracks_changed_content": ["Chrono Trigger"],
|
||||
"soundtracks_removed": ["Old Soundtrack"],
|
||||
"catched_errors": [],
|
||||
"total_time": "00:00:03",
|
||||
"total_time": "00:03",
|
||||
}
|
||||
|
||||
## The server's version history, newest first.
|
||||
@@ -72,25 +72,6 @@ func _init(main: Control) -> void:
|
||||
_main = main
|
||||
|
||||
|
||||
## A folder like a ripped two-disc soundtrack, to upload: MP3s in disc folders, and a
|
||||
## FLAC that is left out.
|
||||
static func _upload_folder() -> String:
|
||||
var folder: String = Settings.config_dir().path_join("upload/Chrono Trigger")
|
||||
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
var files: Array[String] = []
|
||||
for disc: int in [1, 2]:
|
||||
for track: int in 4:
|
||||
files.append("Disc %d/%02d %s.mp3" % [disc, track + 1, JAPANESE_SONGS[track]])
|
||||
files.append("Disc 2/05 Bonus.flac")
|
||||
for relative: String in files:
|
||||
var path: String = folder.path_join(relative)
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
file.store_buffer(mp3)
|
||||
file.close()
|
||||
return folder
|
||||
|
||||
|
||||
## Returns a fake server that answers every request the main window makes on the tour.
|
||||
## Call [method FakeHttpTransport.install] before the main window is created.
|
||||
static func make_server() -> FakeHttpTransport:
|
||||
@@ -362,22 +343,6 @@ func run(on_state: Callable) -> void:
|
||||
await on_state.call("popup_SoundtrackStats_cover", stats_popup)
|
||||
stats_popup.stop_editing()
|
||||
stats_popup.close()
|
||||
await _settle()
|
||||
|
||||
var upload_popup: UploadPopup = _node("UploadPopup")
|
||||
upload_popup.show_folder(_upload_folder())
|
||||
await _settle()
|
||||
await on_state.call("popup_UploadWindow", upload_popup)
|
||||
upload_popup._set_uploading(true)
|
||||
upload_popup.upload_progress.max_value = upload_popup.folder.songs.size()
|
||||
upload_popup.upload_progress.value = 3
|
||||
upload_popup.upload_progress.visible = true
|
||||
upload_popup._show_status("Uploading 4 of %d: %s" % [upload_popup.folder.songs.size(),
|
||||
upload_popup.folder.songs[3].name])
|
||||
await _settle()
|
||||
await on_state.call("popup_UploadWindow_uploading", upload_popup)
|
||||
upload_popup._set_uploading(false)
|
||||
upload_popup.close()
|
||||
_main.show_tab(&"quiz")
|
||||
await _settle()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ extends MainWindowTestSuite
|
||||
const MAIN_WINDOW_CHILDREN: Array[String] = [
|
||||
"Background", "TopBar", "Quiz", "PopupBackdrop", "SearchPopup", "SyncPopup", "QRPopup",
|
||||
"ShortcutsPopup", "StatisticsView", "AboutPopup", "LogPopup", "SoundtrackStatsPopup",
|
||||
"SettingsPopup", "UpdatePopup", "UploadPopup",
|
||||
"SettingsPopup", "UpdatePopup",
|
||||
]
|
||||
|
||||
## The parts of the quiz and the scene each one comes from.
|
||||
@@ -226,7 +226,7 @@ func test_click_on_top_bar_while_quiz_popup_is_open_only_closes_it() -> void:
|
||||
|
||||
func test_sync_disables_quiz_buttons_until_finished() -> void:
|
||||
_server.respond_bytes("/sync", PackedByteArray())
|
||||
_server.respond_json("/sync/progress", {"progress": 10, "time_spent": "00:00:01"})
|
||||
_server.respond_json("/sync/progress", {"progress": 10, "time_spent": "00:01"})
|
||||
var sync_popup: BasePopup = _window.get_node("SyncPopup")
|
||||
|
||||
_top_bar_button("SyncButton").pressed.emit()
|
||||
|
||||
@@ -10,6 +10,8 @@ var _window: Control
|
||||
func before_test() -> void:
|
||||
_snapshot = AutoloadSnapshot.take([Settings])
|
||||
Settings.currently_syncing = false
|
||||
# The bar jumps to its values and the result shows at once
|
||||
Settings.reduce_animations = true
|
||||
_server = FakeHttpTransport.new().install()
|
||||
_server.respond_bytes("/sync", PackedByteArray())
|
||||
_runner = scene_runner("res://popups/SyncPopup.tscn")
|
||||
@@ -38,7 +40,7 @@ func _empty_result() -> Dictionary:
|
||||
"soundtracks_changed_content": [],
|
||||
"soundtracks_removed": [],
|
||||
"catched_errors": [],
|
||||
"total_time": "00:00:03",
|
||||
"total_time": "00:03",
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +66,29 @@ func test_start_sync_does_nothing_while_already_syncing() -> void:
|
||||
|
||||
func test_progress_updates_progress_bar() -> void:
|
||||
_runner.invoke("start_sync")
|
||||
_server.respond_json("/sync/progress", {"progress": "42", "time_spent": "00:00:12"})
|
||||
_server.respond_json("/sync/progress", {"progress": "42", "time_spent": "00:12"})
|
||||
|
||||
_runner.invoke("_on_sync_timeout")
|
||||
|
||||
assert_float(_node("SyncProgressBar").value).is_equal(42.0)
|
||||
assert_str(_node("SyncTime").text).is_equal("00:00:12")
|
||||
assert_str(_node("SyncTime").text).is_equal("00:12")
|
||||
|
||||
|
||||
func test_finished_sync_fills_the_bar_before_the_result() -> void:
|
||||
Settings.reduce_animations = false
|
||||
_runner.invoke("start_sync")
|
||||
var bar: AnimatedProgressBar = _node("SyncProgressBar")
|
||||
assert_bool(bar.indeterminate).is_true()
|
||||
|
||||
_finish_sync(_empty_result())
|
||||
|
||||
assert_bool(_node("ProgressHBoxContainer").visible).is_true()
|
||||
await await_millis(int(Anim.SLOW * 1000) + 100)
|
||||
assert_float(bar.value).is_equal(100.0)
|
||||
assert_bool(_node("ProgressHBoxContainer").visible).is_true()
|
||||
await await_millis(int(Anim.SLOW * 1000) + 100)
|
||||
assert_bool(_node("ProgressHBoxContainer").visible).is_false()
|
||||
assert_str(_node("SyncTitleLabel").text).is_equal("Sync Result")
|
||||
|
||||
|
||||
func test_finished_sync_shows_only_sections_with_changes() -> void:
|
||||
@@ -88,7 +107,7 @@ func test_finished_sync_shows_only_sections_with_changes() -> void:
|
||||
assert_bool(_node("NoChangesLabel").visible).is_false()
|
||||
# Header plus one label per soundtrack
|
||||
assert_int(_node("SoundtracksAddedVBox").get_child_count()).is_equal(3)
|
||||
assert_str(_node("SyncTime").text).is_equal("Total time: 00:00:03")
|
||||
assert_str(_node("SyncTime").text).is_equal("Total time: 00:03")
|
||||
assert_bool(Settings.currently_syncing).is_false()
|
||||
await assert_signal(_window).is_emitted("sync_finished")
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ const STYLES: Dictionary = {
|
||||
"PlayerTime": "Label",
|
||||
"PlayerIcon": "Button",
|
||||
"UpdateButton": "Button",
|
||||
"SyncProgressBar": "ProgressBar",
|
||||
"SyncProgressBarFill": "Panel",
|
||||
"SyncProgressBarText": "Label",
|
||||
"ProgressSlider": "HSlider",
|
||||
"VolumeBar": "HSlider",
|
||||
"StatisticsCard": "PanelContainer",
|
||||
@@ -67,6 +70,7 @@ const STYLES: Dictionary = {
|
||||
## Theme types that only scripts read from, so they are not styles a node can pick.
|
||||
const CODE_ONLY_TYPES: Dictionary = {
|
||||
"Palette": ["backdrop", "first_point", "match_point", "flash"],
|
||||
"SyncProgressBar": ["shine"],
|
||||
"ScoreGraph": [
|
||||
"player_1", "player_2", "player_3", "player_4", "player_5", "player_6", "player_7",
|
||||
"grid", "axis_text", "winning_line",
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
extends MainWindowTestSuite
|
||||
## Adding a soundtrack from a folder: picked in the statistics or dropped on the window,
|
||||
## uploaded one song at a time, and found everywhere afterwards.
|
||||
|
||||
const UPLOADS: String = "/soundtracks/uploads"
|
||||
const UPLOAD_ID: String = "0b5e2b1e-3a4f-4d5e-9f00-000000000001"
|
||||
|
||||
var _popup: UploadPopup
|
||||
var _dir: String
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
await super()
|
||||
_popup = _window.get_node("UploadPopup")
|
||||
_dir = create_temp_dir("upload_%d" % randi()).path_join("Chrono Trigger")
|
||||
_song("01 Title.mp3", "ID3 title")
|
||||
_song("Disc 2/01 Ending.mp3", "ID3 ending")
|
||||
_song("02 Theme.flac", "flac")
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
Settings.currently_syncing = false
|
||||
await super()
|
||||
|
||||
|
||||
func _song(relative: String, content: String) -> void:
|
||||
var path: String = _dir.path_join(relative)
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
file.store_string(content)
|
||||
file.close()
|
||||
|
||||
|
||||
func _file_path(file_name: String) -> String:
|
||||
return "%s/%s/files/%s" % [UPLOADS, UPLOAD_ID, file_name.uri_encode()]
|
||||
|
||||
|
||||
func _server_accepts_everything() -> void:
|
||||
_server.respond_json(UPLOADS, {"id": UPLOAD_ID, "name": "Chrono Trigger", "files": []},
|
||||
201)
|
||||
_server.respond_json(_file_path("01 Title.mp3"), {"name": "01 Title.mp3", "size": 9})
|
||||
_server.respond_json(_file_path("01 Ending.mp3"), {"name": "01 Ending.mp3", "size": 10})
|
||||
_server.respond_json("%s/%s/finish" % [UPLOADS, UPLOAD_ID], {"uuid": "st-new",
|
||||
"name": "Chrono Trigger", "songs": [{"name": "01 Title"}, {"name": "01 Ending"}]})
|
||||
_server.respond_bytes("%s/%s" % [UPLOADS, UPLOAD_ID], PackedByteArray(), 204)
|
||||
|
||||
|
||||
func _upload_requests() -> Array[String]:
|
||||
var paths: Array[String] = []
|
||||
for path: String in _server.requested_paths:
|
||||
if path.begins_with(UPLOADS):
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
|
||||
func test_folder_lists_its_songs_and_what_is_left_out() -> void:
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
assert_bool(_popup.visible).is_true()
|
||||
assert_str(_popup.name_field.text).is_equal("Chrono Trigger")
|
||||
assert_int(_popup.song_list.get_child_count()).is_equal(3)
|
||||
assert_str(_popup.summary_label.text).starts_with("2 songs, ")
|
||||
assert_str(_popup.summary_label.text).contains("1 file left out")
|
||||
assert_bool(_popup.upload_button.disabled).is_false()
|
||||
|
||||
|
||||
func test_upload_sends_each_song_then_finishes() -> void:
|
||||
_server_accepts_everything()
|
||||
var added: Array[String] = []
|
||||
_popup.soundtrack_added.connect(
|
||||
func(soundtrack: SoundtrackWithSongList) -> void: added.append(soundtrack.name))
|
||||
var searches_before: int = _server.count("/soundtracks")
|
||||
_popup.show_folder(_dir)
|
||||
_popup.name_field.text = "Chrono Trigger (SNES)"
|
||||
|
||||
_popup.upload()
|
||||
|
||||
assert_array(_upload_requests()).contains_exactly([UPLOADS,
|
||||
_file_path("01 Title.mp3"), _file_path("01 Ending.mp3"),
|
||||
"%s/%s/finish" % [UPLOADS, UPLOAD_ID]])
|
||||
var start: int = _server.requested_paths.find(UPLOADS)
|
||||
assert_str(JSON.parse_string(_server.requested_bodies[start]).name) \
|
||||
.is_equal("Chrono Trigger (SNES)")
|
||||
var first_song: int = _server.requested_paths.find(_file_path("01 Title.mp3"))
|
||||
assert_int(_server.requested_methods[first_song]).is_equal(HTTPClient.METHOD_PUT)
|
||||
assert_str(_server.requested_bodies[first_song]).is_equal("ID3 title")
|
||||
assert_array(_server.requested_headers[first_song]).contains(["Content-Type: audio/mpeg"])
|
||||
assert_array(added).contains_exactly(["Chrono Trigger"])
|
||||
assert_str(_popup.status_label.text).is_equal("Added Chrono Trigger with 2 songs.")
|
||||
assert_bool(_popup.upload_button.visible).is_false()
|
||||
assert_str(_popup.cancel_button.text).is_equal("Close")
|
||||
# Search finds the new soundtrack
|
||||
assert_int(_server.count("/soundtracks")).is_greater(searches_before)
|
||||
|
||||
|
||||
func test_song_that_fails_once_is_sent_again() -> void:
|
||||
_server_accepts_everything()
|
||||
_server.fail_first(_file_path("01 Title.mp3"), 1)
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
_popup.upload()
|
||||
|
||||
assert_int(_server.count(_file_path("01 Title.mp3"))).is_equal(2)
|
||||
assert_int(_server.count("%s/%s/finish" % [UPLOADS, UPLOAD_ID])).is_equal(1)
|
||||
|
||||
|
||||
func test_song_that_keeps_failing_cancels_the_upload() -> void:
|
||||
_server_accepts_everything()
|
||||
_server.respond_json(_file_path("01 Title.mp3"),
|
||||
{"error": "\"01 Title.mp3\" is not an MP3 file"}, 400)
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
_popup.upload()
|
||||
|
||||
assert_int(_server.count(_file_path("01 Title.mp3"))).is_equal(UploadPopup.RETRIES + 1)
|
||||
var cancel: int = _server.requested_paths.find("%s/%s" % [UPLOADS, UPLOAD_ID])
|
||||
assert_int(_server.requested_methods[cancel]).is_equal(HTTPClient.METHOD_DELETE)
|
||||
assert_int(_server.count("%s/%s/finish" % [UPLOADS, UPLOAD_ID])).is_equal(0)
|
||||
assert_str(_popup.status_label.text).is_equal(
|
||||
"Could not upload 01 Title.mp3: \"01 Title.mp3\" is not an MP3 file.")
|
||||
assert_bool(_popup.upload_button.disabled).is_false()
|
||||
|
||||
|
||||
func test_taken_name_says_so() -> void:
|
||||
_server.respond_json(UPLOADS,
|
||||
{"error": "already exists: a soundtrack called \"Chrono Trigger\""}, 409)
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
_popup.upload()
|
||||
|
||||
assert_str(_popup.status_label.text).contains("a soundtrack called \"Chrono Trigger\"")
|
||||
assert_array(_upload_requests()).contains_exactly([UPLOADS])
|
||||
assert_bool(_popup.name_field.editable).is_true()
|
||||
|
||||
|
||||
func test_closing_during_the_upload_cancels_it() -> void:
|
||||
_server_accepts_everything()
|
||||
_server.hold(_file_path("01 Title.mp3"))
|
||||
_popup.show_folder(_dir)
|
||||
_popup.upload()
|
||||
|
||||
_popup.close()
|
||||
_server.release(_file_path("01 Title.mp3"))
|
||||
|
||||
assert_int(_server.count("%s/%s" % [UPLOADS, UPLOAD_ID])).is_equal(1)
|
||||
assert_int(_server.count(_file_path("01 Ending.mp3"))).is_equal(0)
|
||||
|
||||
|
||||
func test_no_upload_while_the_server_syncs() -> void:
|
||||
Settings.currently_syncing = true
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
_popup.upload()
|
||||
|
||||
assert_array(_upload_requests()).is_empty()
|
||||
assert_str(_popup.status_label.text).contains("syncing")
|
||||
|
||||
|
||||
func test_folder_without_mp3s_cannot_be_uploaded() -> void:
|
||||
var empty: String = _dir.get_base_dir().path_join("Empty")
|
||||
DirAccess.make_dir_recursive_absolute(empty)
|
||||
|
||||
_popup.show_folder(empty)
|
||||
|
||||
assert_bool(_popup.upload_button.disabled).is_true()
|
||||
assert_str(_popup.status_label.text).contains("no MP3 files")
|
||||
|
||||
|
||||
func test_empty_name_cannot_be_uploaded() -> void:
|
||||
_popup.show_folder(_dir)
|
||||
|
||||
_popup.name_field.text = " .. "
|
||||
_popup.name_field.text_changed.emit(_popup.name_field.text)
|
||||
|
||||
assert_bool(_popup.upload_button.disabled).is_true()
|
||||
|
||||
|
||||
func test_dropping_a_folder_opens_it() -> void:
|
||||
_window.on_files_dropped(PackedStringArray([_dir]))
|
||||
|
||||
assert_bool(_popup.visible).is_true()
|
||||
assert_str(_popup.folder.path).is_equal(_dir)
|
||||
|
||||
|
||||
func test_dropping_songs_opens_their_folder() -> void:
|
||||
_window.on_files_dropped(PackedStringArray([_dir.path_join("01 Title.mp3")]))
|
||||
|
||||
assert_str(_popup.folder.path).is_equal(_dir)
|
||||
|
||||
|
||||
func test_statistics_has_the_button() -> void:
|
||||
var statistics: StatisticsView = _window.get_node("StatisticsView")
|
||||
|
||||
assert_bool(statistics.add_soundtrack_pressed.is_connected(_popup.choose_folder)).is_true()
|
||||
assert_str(statistics.add_soundtrack_button.text).is_equal("Add soundtrack…")
|
||||
|
||||
|
||||
@warning_ignore("unused_parameter")
|
||||
func test_short_path(path: String, shown: String, test_parameters: Array = [
|
||||
["/home/me/Music/Chrono Trigger", "…/Music/Chrono Trigger"],
|
||||
["C:\\Music\\Soundtracks\\Chrono Trigger", "…/Soundtracks/Chrono Trigger"],
|
||||
["/Music", "/Music"],
|
||||
]) -> void:
|
||||
assert_str(UploadPopup.short_path(path)).is_equal(shown)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bcr68ps41ex1r
|
||||
@@ -0,0 +1,88 @@
|
||||
extends GdUnitTestSuite
|
||||
## The sync popup's progress bar: busy until there is progress, gliding to new values,
|
||||
## a highlight on the filled part, and plain jumps with Reduce animations.
|
||||
|
||||
var _runner: GdUnitSceneRunner
|
||||
var _bar: AnimatedProgressBar
|
||||
var _reduce_before: bool
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_reduce_before = Settings.reduce_animations
|
||||
Settings.reduce_animations = false
|
||||
var holder: Control = auto_free(Control.new())
|
||||
holder.size = Vector2(1000, 100)
|
||||
_bar = AnimatedProgressBar.new()
|
||||
_bar.theme_type_variation = &"SyncProgressBar"
|
||||
_bar.size = Vector2(1000, 32)
|
||||
holder.add_child(_bar)
|
||||
_runner = scene_runner(holder)
|
||||
await _runner.simulate_frames(1)
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
Settings.reduce_animations = _reduce_before
|
||||
|
||||
|
||||
func _label() -> Label:
|
||||
return _bar.get_child(1) as Label
|
||||
|
||||
|
||||
func test_waits_with_the_busy_animation() -> void:
|
||||
_bar.wait()
|
||||
|
||||
assert_bool(_bar.indeterminate).is_true()
|
||||
assert_str(_label().text).is_empty()
|
||||
|
||||
|
||||
func test_progress_ends_the_wait_and_glides() -> void:
|
||||
_bar.wait()
|
||||
|
||||
_bar.move_to(40)
|
||||
|
||||
assert_bool(_bar.indeterminate).is_false()
|
||||
assert_float(_bar.value).is_less(40.0)
|
||||
await await_millis(int(Anim.SLOW * 1000) + 150)
|
||||
assert_float(_bar.value).is_equal(40.0)
|
||||
assert_str(_label().text).is_equal("40%")
|
||||
|
||||
|
||||
func test_reduce_animations_jumps_and_never_waits() -> void:
|
||||
Settings.reduce_animations = true
|
||||
|
||||
_bar.wait()
|
||||
assert_bool(_bar.indeterminate).is_false()
|
||||
_bar.move_to(75)
|
||||
|
||||
assert_float(_bar.value).is_equal(75.0)
|
||||
assert_str(_label().text).is_equal("75%")
|
||||
|
||||
|
||||
func test_highlight_stays_on_the_filled_part() -> void:
|
||||
Settings.reduce_animations = true
|
||||
_bar.move_to(50)
|
||||
Settings.reduce_animations = false
|
||||
await _runner.simulate_frames(3)
|
||||
|
||||
var mask: Control = _bar.get_child(0)
|
||||
assert_bool(mask.visible).is_true()
|
||||
assert_float(mask.size.x).is_equal(_bar.fill_width())
|
||||
assert_float(_bar.fill_width()).is_between(490.0, 510.0)
|
||||
|
||||
|
||||
func test_no_highlight_without_progress_or_animations() -> void:
|
||||
await _runner.simulate_frames(2)
|
||||
assert_bool((_bar.get_child(0) as Control).visible).is_false()
|
||||
|
||||
Settings.reduce_animations = true
|
||||
_bar.move_to(50)
|
||||
await _runner.simulate_frames(2)
|
||||
assert_bool((_bar.get_child(0) as Control).visible).is_false()
|
||||
|
||||
|
||||
func test_highlight_takes_the_themes_colour() -> void:
|
||||
var shine: TextureRect = _bar.get_child(0).get_child(0)
|
||||
var gradient: Gradient = (shine.texture as GradientTexture2D).gradient
|
||||
|
||||
assert_object(gradient.get_color(1)).is_equal(_bar.get_theme_color(&"shine"))
|
||||
assert_float(gradient.get_color(0).a).is_equal(0.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgb6no4urwpc7
|
||||
@@ -118,7 +118,7 @@ func test_sync_response() -> void:
|
||||
"soundtracks_added": ["Terranigma"], "soundtracks_re_added": [],
|
||||
"soundtracks_changed_title": {"FF6": "Final Fantasy VI"},
|
||||
"soundtracks_changed_content": ["Chrono Trigger"], "soundtracks_removed": ["Old"],
|
||||
"catched_errors": ["Could not read x.mp3"], "total_time": "00:00:03"}"""))
|
||||
"catched_errors": ["Could not read x.mp3"], "total_time": "00:03"}"""))
|
||||
|
||||
assert_array(result.soundtracks_added).contains_exactly(["Terranigma"])
|
||||
assert_array(result.soundtracks_re_added).is_empty()
|
||||
@@ -126,15 +126,15 @@ func test_sync_response() -> void:
|
||||
assert_array(result.soundtracks_changed_content).contains_exactly(["Chrono Trigger"])
|
||||
assert_array(result.soundtracks_removed).contains_exactly(["Old"])
|
||||
assert_array(result.catched_errors).contains_exactly(["Could not read x.mp3"])
|
||||
assert_str(result.total_time).is_equal("00:00:03")
|
||||
assert_str(result.total_time).is_equal("00:03")
|
||||
|
||||
|
||||
func test_progress_response() -> void:
|
||||
var progress: ProgressResponse = ProgressResponse.from_dict(_parse("""{
|
||||
"progress": "40", "time_spent": "00:00:02"}"""))
|
||||
"progress": "40", "time_spent": "00:02"}"""))
|
||||
|
||||
assert_str(progress.progress).is_equal("40")
|
||||
assert_str(progress.time_spent).is_equal("00:00:02")
|
||||
assert_str(progress.time_spent).is_equal("00:02")
|
||||
|
||||
|
||||
func test_version_data() -> void:
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
extends GdUnitTestSuite
|
||||
## Reading a folder to upload as a soundtrack: only MP3s are sent, disc folders are
|
||||
## flattened with the disc's number first, and other song files are listed as left out.
|
||||
|
||||
var _dir: String
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_dir = create_temp_dir("upload_folder_%d" % randi()).path_join("Chrono Trigger")
|
||||
DirAccess.make_dir_recursive_absolute(_dir)
|
||||
|
||||
|
||||
func _file(relative: String, size: int = 10) -> void:
|
||||
var path: String = _dir.path_join(relative)
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var bytes: PackedByteArray = PackedByteArray()
|
||||
bytes.resize(size)
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
|
||||
|
||||
func _names(folder: UploadFolder) -> Array:
|
||||
return folder.songs.map(func(song: Dictionary) -> String: return song.name)
|
||||
|
||||
|
||||
func test_name_comes_from_the_folder() -> void:
|
||||
_file("01 Title.mp3")
|
||||
|
||||
assert_str(UploadFolder.scan(_dir).suggested_name).is_equal("Chrono Trigger")
|
||||
assert_str(UploadFolder.scan(_dir + "/").suggested_name).is_equal("Chrono Trigger")
|
||||
|
||||
|
||||
func test_songs_in_natural_order_with_sizes() -> void:
|
||||
_file("10 Ending.mp3", 300)
|
||||
_file("2 Peaceful Days.MP3", 200)
|
||||
_file("1 Title.mp3", 100)
|
||||
|
||||
var folder: UploadFolder = UploadFolder.scan(_dir)
|
||||
|
||||
assert_array(_names(folder)).contains_exactly(
|
||||
["1 Title.mp3", "2 Peaceful Days.MP3", "10 Ending.mp3"])
|
||||
assert_int(folder.songs[0].size).is_equal(100)
|
||||
assert_int(folder.total_size).is_equal(600)
|
||||
assert_str(folder.songs[0].path).is_equal(_dir.path_join("1 Title.mp3"))
|
||||
|
||||
|
||||
func test_disc_folders_are_flattened_with_their_number() -> void:
|
||||
_file("Disc 10/01 Last.mp3")
|
||||
_file("Disc 2/01 Middle.mp3")
|
||||
_file("Disc 1/01 First.mp3")
|
||||
_file("Disc 1/Bonus/01 Extra.mp3")
|
||||
|
||||
var folder: UploadFolder = UploadFolder.scan(_dir)
|
||||
|
||||
assert_array(_names(folder)).contains_exactly(
|
||||
["1-01 First.mp3", "1-01 Extra.mp3", "2-01 Middle.mp3", "3-01 Last.mp3"])
|
||||
|
||||
|
||||
func test_one_disc_folder_keeps_the_names() -> void:
|
||||
_file("CD1/01 Title.mp3")
|
||||
_file("CD1/02 Theme.mp3")
|
||||
_file("Scans/cover.jpg")
|
||||
|
||||
assert_array(_names(UploadFolder.scan(_dir))).contains_exactly(
|
||||
["01 Title.mp3", "02 Theme.mp3"])
|
||||
|
||||
|
||||
func test_folders_without_songs_are_not_discs() -> void:
|
||||
_file("Artwork/front.jpg")
|
||||
_file("Disc 1/01 First.mp3")
|
||||
_file("Disc 2/01 Second.mp3")
|
||||
|
||||
assert_array(_names(UploadFolder.scan(_dir))).contains_exactly(
|
||||
["1-01 First.mp3", "2-01 Second.mp3"])
|
||||
|
||||
|
||||
func test_other_songs_are_listed_as_left_out_and_the_rest_ignored() -> void:
|
||||
_file("01 Title.mp3")
|
||||
_file("02 Theme.flac")
|
||||
_file("Disc 2/01 Song.ogg")
|
||||
_file("cover.jpg")
|
||||
_file("notes.txt")
|
||||
|
||||
var folder: UploadFolder = UploadFolder.scan(_dir)
|
||||
|
||||
assert_array(_names(folder)).contains_exactly(["01 Title.mp3"])
|
||||
assert_array(folder.skipped).contains_exactly(["02 Theme.flac", "Disc 2/01 Song.ogg"])
|
||||
|
||||
|
||||
func test_hidden_files_and_folders_are_left_out() -> void:
|
||||
_file("01 Title.mp3")
|
||||
_file("._01 Title.mp3")
|
||||
_file(".sync/01 Copy.mp3")
|
||||
|
||||
assert_array(_names(UploadFolder.scan(_dir))).contains_exactly(["01 Title.mp3"])
|
||||
|
||||
|
||||
func test_same_name_twice_gets_a_number() -> void:
|
||||
_file("Disc 1/01 Title.mp3")
|
||||
_file("Disc 1/Extra/01 Title.mp3")
|
||||
|
||||
assert_array(_names(UploadFolder.scan(_dir))).contains_exactly(
|
||||
["01 Title.mp3", "01 Title (2).mp3"])
|
||||
|
||||
|
||||
func test_empty_folder_has_no_songs() -> void:
|
||||
_file("cover.jpg")
|
||||
|
||||
assert_bool(UploadFolder.scan(_dir).has_songs()).is_false()
|
||||
|
||||
|
||||
@warning_ignore("unused_parameter")
|
||||
func test_clean_name(name: String, cleaned: String, test_parameters: Array = [
|
||||
["Chrono Trigger", "Chrono Trigger"],
|
||||
["Re:Zero", "Re-Zero"],
|
||||
["What? <Live>", "What- -Live-"],
|
||||
["AC/DC", "AC-DC"],
|
||||
[" .hidden name. ", "hidden name"],
|
||||
["クロノ・トリガー", "クロノ・トリガー"],
|
||||
]) -> void:
|
||||
assert_str(UploadFolder.clean_name(name)).is_equal(cleaned)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dlaocvvknrr73
|
||||
@@ -1248,6 +1248,30 @@ corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_track"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.478, 0.082, 0.125, 0.7)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_fill"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.204, 0.553, 0.290, 1.0)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
corner_detail = 8
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_default_font")
|
||||
Button/colors/font_color = Color(0.478, 0.082, 0.125, 1.0)
|
||||
@@ -1456,6 +1480,21 @@ ProgressBar/constants/outline_size = 0
|
||||
ProgressBar/font_sizes/font_size = 16
|
||||
ProgressBar/styles/background = SubResource("StyleBoxFlat_07kyi")
|
||||
ProgressBar/styles/fill = SubResource("StyleBoxFlat_ouuwv")
|
||||
SyncProgressBar/base_type = &"ProgressBar"
|
||||
SyncProgressBar/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBar/colors/font_outline_color = Color(0, 0, 0, 0.45)
|
||||
SyncProgressBar/colors/shine = Color(0.949, 0.843, 0.478, 0.9)
|
||||
SyncProgressBar/constants/outline_size = 4
|
||||
SyncProgressBar/font_sizes/font_size = 18
|
||||
SyncProgressBar/styles/background = SubResource("StyleBoxFlat_sync_track")
|
||||
SyncProgressBar/styles/fill = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarFill/base_type = &"Panel"
|
||||
SyncProgressBarFill/styles/panel = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarText/base_type = &"Label"
|
||||
SyncProgressBarText/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBarText/colors/font_outline_color = Color(0, 0, 0, 0.45)
|
||||
SyncProgressBarText/constants/outline_size = 4
|
||||
SyncProgressBarText/font_sizes/font_size = 18
|
||||
ScoreGraph/colors/axis_text = Color(0.965, 0.925, 0.878, 1.0)
|
||||
ScoreGraph/colors/grid = Color(0.992, 0.980, 0.953, 0.1)
|
||||
ScoreGraph/colors/player_1 = Color(0.992, 0.980, 0.953, 1.0)
|
||||
|
||||
@@ -1056,6 +1056,30 @@ corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_track"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.1, 0.1, 0.1, 0.55)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_fill"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(1, 0.5, 0, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
corner_detail = 8
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_default_font")
|
||||
Button/colors/font_color = Color(0.875, 0.875, 0.875, 1)
|
||||
@@ -1264,6 +1288,21 @@ ProgressBar/constants/outline_size = 0
|
||||
ProgressBar/font_sizes/font_size = 16
|
||||
ProgressBar/styles/background = SubResource("StyleBoxFlat_07kyi")
|
||||
ProgressBar/styles/fill = SubResource("StyleBoxFlat_ouuwv")
|
||||
SyncProgressBar/base_type = &"ProgressBar"
|
||||
SyncProgressBar/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBar/colors/font_outline_color = Color(0, 0, 0, 0.55)
|
||||
SyncProgressBar/colors/shine = Color(1, 0.86, 0.6, 0.8)
|
||||
SyncProgressBar/constants/outline_size = 4
|
||||
SyncProgressBar/font_sizes/font_size = 18
|
||||
SyncProgressBar/styles/background = SubResource("StyleBoxFlat_sync_track")
|
||||
SyncProgressBar/styles/fill = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarFill/base_type = &"Panel"
|
||||
SyncProgressBarFill/styles/panel = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarText/base_type = &"Label"
|
||||
SyncProgressBarText/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBarText/colors/font_outline_color = Color(0, 0, 0, 0.55)
|
||||
SyncProgressBarText/constants/outline_size = 4
|
||||
SyncProgressBarText/font_sizes/font_size = 18
|
||||
ScoreGraph/colors/axis_text = Color(0.75, 0.75, 0.75, 1)
|
||||
ScoreGraph/colors/grid = Color(1, 1, 1, 0.1)
|
||||
ScoreGraph/colors/player_1 = Color(0.25, 0.56, 0.95, 1)
|
||||
|
||||
@@ -1231,6 +1231,30 @@ corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_track"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.118, 0.290, 0.412, 0.7)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_fill"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.533, 0.753, 0.816, 1.0)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
corner_detail = 8
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_default_font")
|
||||
Button/colors/font_color = Color(0.925, 0.937, 0.957, 1.0)
|
||||
@@ -1439,6 +1463,21 @@ ProgressBar/constants/outline_size = 0
|
||||
ProgressBar/font_sizes/font_size = 16
|
||||
ProgressBar/styles/background = SubResource("StyleBoxFlat_07kyi")
|
||||
ProgressBar/styles/fill = SubResource("StyleBoxFlat_ouuwv")
|
||||
SyncProgressBar/base_type = &"ProgressBar"
|
||||
SyncProgressBar/colors/font_color = Color(0.925, 0.937, 0.957, 1.0)
|
||||
SyncProgressBar/colors/font_outline_color = Color(0.180, 0.204, 0.251, 0.8)
|
||||
SyncProgressBar/colors/shine = Color(0.925, 0.937, 0.957, 0.85)
|
||||
SyncProgressBar/constants/outline_size = 4
|
||||
SyncProgressBar/font_sizes/font_size = 18
|
||||
SyncProgressBar/styles/background = SubResource("StyleBoxFlat_sync_track")
|
||||
SyncProgressBar/styles/fill = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarFill/base_type = &"Panel"
|
||||
SyncProgressBarFill/styles/panel = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarText/base_type = &"Label"
|
||||
SyncProgressBarText/colors/font_color = Color(0.925, 0.937, 0.957, 1.0)
|
||||
SyncProgressBarText/colors/font_outline_color = Color(0.180, 0.204, 0.251, 0.8)
|
||||
SyncProgressBarText/constants/outline_size = 4
|
||||
SyncProgressBarText/font_sizes/font_size = 18
|
||||
ScoreGraph/colors/axis_text = Color(0.847, 0.871, 0.914, 1.0)
|
||||
ScoreGraph/colors/grid = Color(0.925, 0.937, 0.957, 0.1)
|
||||
ScoreGraph/colors/player_1 = Color(0.533, 0.753, 0.816, 1.0)
|
||||
|
||||
@@ -1157,6 +1157,30 @@ corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_track"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.86, 0.86, 0.86, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
corner_detail = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sync_fill"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(1, 0.612, 0, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
corner_detail = 8
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_default_font")
|
||||
Button/colors/font_color = Color(1, 0.612, 0, 1)
|
||||
@@ -1365,6 +1389,21 @@ ProgressBar/constants/outline_size = 0
|
||||
ProgressBar/font_sizes/font_size = 16
|
||||
ProgressBar/styles/background = SubResource("StyleBoxFlat_07kyi")
|
||||
ProgressBar/styles/fill = SubResource("StyleBoxFlat_ouuwv")
|
||||
SyncProgressBar/base_type = &"ProgressBar"
|
||||
SyncProgressBar/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBar/colors/font_outline_color = Color(0, 0, 0, 0.45)
|
||||
SyncProgressBar/colors/shine = Color(1, 1, 1, 0.7)
|
||||
SyncProgressBar/constants/outline_size = 4
|
||||
SyncProgressBar/font_sizes/font_size = 18
|
||||
SyncProgressBar/styles/background = SubResource("StyleBoxFlat_sync_track")
|
||||
SyncProgressBar/styles/fill = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarFill/base_type = &"Panel"
|
||||
SyncProgressBarFill/styles/panel = SubResource("StyleBoxFlat_sync_fill")
|
||||
SyncProgressBarText/base_type = &"Label"
|
||||
SyncProgressBarText/colors/font_color = Color(1, 1, 1, 1)
|
||||
SyncProgressBarText/colors/font_outline_color = Color(0, 0, 0, 0.45)
|
||||
SyncProgressBarText/constants/outline_size = 4
|
||||
SyncProgressBarText/font_sizes/font_size = 18
|
||||
ScoreGraph/colors/axis_text = Color(0.75, 0.75, 0.75, 1)
|
||||
ScoreGraph/colors/grid = Color(1, 1, 1, 0.1)
|
||||
ScoreGraph/colors/player_1 = Color(0.25, 0.56, 0.95, 1)
|
||||
|
||||
Reference in new issue
Block a user