Files
MusicPlayer/common/soundtrack_search.gd
T
Sansan a21b6e9b22
Build / test (push) Successful in 40s
Build / build (push) Successful in 1m16s
Add a Statistics tab with a card for every soundtrack
Part of #41. The top bar has Quiz and Statistics tabs instead of the
Statistics button. The Statistics tab replaces the quiz below the top bar
(the song keeps playing) and shows every soundtrack as a card with its
plays, last played date and most and least played songs, loaded from the
new GET /api/v1/statistics/soundtracks. The cards can be searched (with
the Search popup's matching, now in SoundtrackSearch), filtered on played
or never played, and sorted A-Z, Z-A, most or least played. Clicking a
card opens a popup with all its songs.

Screenshots of the other states only differ in the top bar, and the
layout baseline is taken again with the new statistics states.
2026-10-06 23:33:00 +02:00

69 lines
2.6 KiB
GDScript

class_name SoundtrackSearch
extends RefCounted
## Finds soundtrack names matching a search text. Matches ignore case, spaces and
## special characters, and also find the letters in order with others in between.
##
## Used by the Search popup and the statistics view, so both find the same soundtracks.
## Each name as a Dictionary with its original, lowercase and cleaned form.
var _names: Array[Dictionary] = []
## Compiled patterns for the letters-in-order search, by cleaned search term.
var _regex_cache: Dictionary = {}
## Replaces the names to search in.
func set_names(names: Array) -> void:
_names.clear()
for soundtrack: String in names:
_names.append({
"original": soundtrack,
"lowercase": soundtrack.to_lower(),
"cleaned": clean_term(soundtrack),
})
## Returns the names matching [param search_text], best matches first: plain
## case-insensitive matches, then matches ignoring spaces and special characters,
## then matches with the letters in order but not next to each other.
func find_matches(search_text: String) -> Array[String]:
var seen: Dictionary = {}
var search_text_lower: String = search_text.to_lower()
var clean_search_text: String = clean_term(search_text)
var regex: RegEx = _compile_regex_cached(clean_search_text)
var passes: Array[Callable] = [
func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower),
func(entry: Dictionary) -> bool: return entry.cleaned.contains(clean_search_text),
func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null,
]
var matches: Array[String] = []
for matches_pass: Callable in passes:
for entry: Dictionary in _names:
if !seen.has(entry.original) && matches_pass.call(entry):
seen[entry.original] = true
matches.append(entry.original)
return matches
## Cleans a term by removing spaces and replacing special characters.
## Matches MusicServer's clean_term() function.
static func clean_term(term: String) -> String:
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \
.replace("'n", "and").to_lower()
# Compiles the letters-in-order pattern for [param search_term], once per term
func _compile_regex_cached(search_term: String) -> RegEx:
if not search_term in _regex_cache:
var regex: RegEx = RegEx.new()
var regex_text: String = ".*"
for letter: String in search_term:
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
if "\\^$.|?*+()[]{}".contains(letter):
regex_text += "\\"
regex_text += letter + ".*"
regex.compile(regex_text)
_regex_cache[search_term] = regex
return _regex_cache[search_term]