Settings has a Music folder row. A folder with subfolders is played as soundtracks: Next picks a random subfolder and a random song in it, the subfolder is the answer's soundtrack and the file name without its type the song. The inspiration list and Search show the subfolders. A folder with songs and no subfolders is a prepared quiz, played once in order (natural sort) or at random, showing only the song title. It has no winning score: after the last song Next becomes Show winner, and the players with the most points win, ties included. While a folder is used nothing but the characters is asked from the server: Sync, QR, Statistics, the update check and play counting are off. Characters fall back to the cached pictures when the server can't be reached. mp3, ogg and wav play; other files and broken ones are skipped. The old local playback code, the hidden Open button and the hidden Local toggle are removed. Next now waits for the song to start before preparing the next one.
157 lines
5.1 KiB
GDScript
157 lines
5.1 KiB
GDScript
class_name SearchPopup
|
|
extends BasePopup
|
|
## Searches every soundtrack on the server as you type, with [SoundtrackSearch], and
|
|
## highlights the letters that matched.
|
|
|
|
## Every soundtrack on the server, sorted by name.
|
|
var soundtracks: Array = []
|
|
|
|
var _matcher: SoundtrackSearch = SoundtrackSearch.new()
|
|
# One row per soundtrack, in the order of soundtracks. Rows are reused between searches.
|
|
var _rows: Array[PanelContainer] = []
|
|
var _index_of: Dictionary = {}
|
|
|
|
@onready var search_panel: Panel = %SearchPanel
|
|
@onready var search_field: LineEdit = %SearchField
|
|
@onready var clear_button: Button = %ClearButton
|
|
@onready var count_label: Label = %CountLabel
|
|
@onready var empty_label: Label = %EmptyLabel
|
|
@onready var search_results: VBoxContainer = %SearchResults
|
|
|
|
|
|
func _ready() -> void:
|
|
clear_button.pressed.connect(clear)
|
|
search_field.text_changed.connect(func(_text: String) -> void: search())
|
|
visibility_changed.connect(_on_visibility_changed)
|
|
|
|
|
|
func close() -> void:
|
|
clear()
|
|
super()
|
|
|
|
|
|
## Empties the search field and shows every soundtrack.
|
|
func clear() -> void:
|
|
search_field.text = ""
|
|
search()
|
|
search_field.grab_focus()
|
|
|
|
|
|
## Shows the soundtracks matching the search field, best matches first, or all of them
|
|
## when it is empty.
|
|
func search() -> void:
|
|
var search_text: String = search_field.text.strip_edges()
|
|
var shown: Array[int] = []
|
|
if search_text.is_empty():
|
|
shown.assign(range(soundtracks.size()))
|
|
else:
|
|
for soundtrack: String in _matcher.find_matches(search_text):
|
|
shown.append(_index_of[soundtrack])
|
|
|
|
var is_shown: Array[bool] = []
|
|
is_shown.resize(_rows.size())
|
|
for position: int in shown.size():
|
|
var row: PanelContainer = _rows[shown[position]]
|
|
if row.get_index() != position:
|
|
search_results.move_child(row, position)
|
|
row.visible = true
|
|
row.theme_type_variation = &"ListRowOdd" if position % 2 != 0 else &"ListRowEven"
|
|
_show_name(row, soundtracks[shown[position]], search_text)
|
|
is_shown[shown[position]] = true
|
|
for index: int in _rows.size():
|
|
if !is_shown[index]:
|
|
_rows[index].visible = false
|
|
|
|
empty_label.visible = shown.is_empty() && !soundtracks.is_empty()
|
|
if search_text.is_empty():
|
|
count_label.text = "%d soundtracks" % soundtracks.size()
|
|
else:
|
|
count_label.text = "%d of %d soundtracks" % [shown.size(), soundtracks.size()]
|
|
|
|
|
|
## The names of the soundtracks shown, in the order they are shown.
|
|
func shown_names() -> Array[String]:
|
|
var names: Array[String] = []
|
|
for row: PanelContainer in search_results.get_children():
|
|
if row.visible:
|
|
names.append(row.get_meta(&"soundtrack"))
|
|
return names
|
|
|
|
|
|
func get_list_of_soundtracks() -> void:
|
|
GameLog.debug("get_list_of_soundtracks")
|
|
if Settings.is_offline():
|
|
set_soundtracks(Settings.music_folder.soundtrack_names())
|
|
return
|
|
var handle_soundtracks: Callable = func handle_soundtracks(array: Variant) -> void:
|
|
if typeof(array) == TYPE_ARRAY:
|
|
set_soundtracks(array)
|
|
else:
|
|
GameLog.warning("Unexpected data")
|
|
MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks)
|
|
|
|
|
|
## Replaces the searchable soundtracks and shows them, keeping the current search.
|
|
func set_soundtracks(list: Array) -> void:
|
|
soundtracks = []
|
|
soundtracks.append_array(list)
|
|
_matcher.set_names(soundtracks)
|
|
_index_of.clear()
|
|
for index: int in range(soundtracks.size() - 1, -1, -1):
|
|
_index_of[soundtracks[index]] = index
|
|
while _rows.size() < soundtracks.size():
|
|
var row: PanelContainer = _make_row()
|
|
search_results.add_child(row)
|
|
_rows.append(row)
|
|
while _rows.size() > soundtracks.size():
|
|
_rows.pop_back().free()
|
|
for index: int in soundtracks.size():
|
|
_rows[index].set_meta(&"soundtrack", soundtracks[index])
|
|
search()
|
|
|
|
|
|
## [param soundtrack_name] as BBCode, with the letters [param search_text] matched in
|
|
## [param highlight_color].
|
|
static func highlighted_name(
|
|
soundtrack_name: String, search_text: String, highlight_color: Color) -> String:
|
|
var matched: Array[int] = SoundtrackSearch.matched_letters(soundtrack_name, search_text)
|
|
var text: String = ""
|
|
var in_highlight: bool = false
|
|
for i: int in soundtrack_name.length():
|
|
var highlight: bool = matched.has(i)
|
|
if highlight != in_highlight:
|
|
text += "[color=#%s]" % highlight_color.to_html(false) if highlight else "[/color]"
|
|
in_highlight = highlight
|
|
var letter: String = soundtrack_name[i]
|
|
text += "[lb]" if letter == "[" else letter
|
|
if in_highlight:
|
|
text += "[/color]"
|
|
return text
|
|
|
|
|
|
func _make_row() -> PanelContainer:
|
|
var row: PanelContainer = PanelContainer.new()
|
|
var label: RichTextLabel = RichTextLabel.new()
|
|
label.theme_type_variation = &"SearchResult"
|
|
label.bbcode_enabled = true
|
|
label.fit_content = true
|
|
label.scroll_active = false
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
row.add_child(label)
|
|
return row
|
|
|
|
|
|
func _show_name(row: PanelContainer, soundtrack_name: String, search_text: String) -> void:
|
|
var text: String = highlighted_name(soundtrack_name, search_text,
|
|
get_theme_color(&"font_color", &"HighlightLarge"))
|
|
var label: RichTextLabel = row.get_child(0)
|
|
# Setting the same text again would make the label lay it out again for nothing
|
|
if label.text != text:
|
|
label.text = text
|
|
|
|
|
|
func _on_visibility_changed() -> void:
|
|
if visible:
|
|
clear()
|