Show every song in the statistics, as an alternative to the cards
A Soundtracks / Songs switch in the statistics toolbar shows a list of every song with its soundtrack and how often it was played. It is searched on both song and soundtrack names, filtered and sorted like the cards, and clicking a song shows its soundtrack's popup. The server has tens of thousands of songs, so the list only makes the rows on screen and reuses them while scrolling, songs are sorted once per load, and searching waits for a pause in typing. Closes #47
This commit is contained in:
1 parent
cd4331881c
commit
a4e6da4c28
14 files changed
+10058
-1670
No files matched your search
@@ -1,7 +1,8 @@
|
||||
class_name StatisticsView
|
||||
extends Panel
|
||||
## Shows every soundtrack on the server as a card with its play statistics. The cards
|
||||
## can be searched, filtered on whether they have been played, and sorted.
|
||||
## Shows every soundtrack on the server as a card with its play statistics, or every
|
||||
## song in a list with its soundtrack. Either can be searched, filtered on whether it has
|
||||
## been played, and sorted.
|
||||
##
|
||||
## Shown instead of the quiz when the Statistics tab is chosen.
|
||||
|
||||
@@ -11,16 +12,27 @@ signal soundtrack_selected(soundtrack: SoundtrackWithSongs)
|
||||
## 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 }
|
||||
|
||||
## Which soundtracks are shown, in the order of the show menu.
|
||||
## Which soundtracks or songs are shown, in the order of the show menu.
|
||||
enum ShowFilter { ALL, PLAYED, NEVER_PLAYED }
|
||||
|
||||
## Whether soundtracks or songs are shown.
|
||||
enum Mode { SOUNDTRACKS, SONGS }
|
||||
|
||||
const CARD_SCENE: PackedScene = preload("res://statistics/SoundtrackCard.tscn")
|
||||
|
||||
## The cards in the order they are shown, hidden ones included.
|
||||
var cards: Array[SoundtrackCard] = []
|
||||
## Whether soundtracks or songs are shown. Change it with [method set_mode].
|
||||
var mode: Mode = Mode.SOUNDTRACKS
|
||||
|
||||
var _search: SoundtrackSearch = SoundtrackSearch.new()
|
||||
var _loading: bool = false
|
||||
# The soundtracks last loaded, by name
|
||||
var _soundtracks: Dictionary[String, SoundtrackWithSongs] = {}
|
||||
# Every song sorted A to Z, made when songs are first shown after loading
|
||||
var _songs_a_to_z: Array[SongInfoForStats] = []
|
||||
var _song_search: SoundtrackSearch = SoundtrackSearch.new()
|
||||
var _songs_ready: bool = false
|
||||
|
||||
@onready var search_field: LineEdit = %SearchField
|
||||
@onready var clear_button: Button = %ClearButton
|
||||
@@ -30,10 +42,19 @@ var _loading: bool = false
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
@onready var retry_button: Button = %RetryButton
|
||||
@onready var card_grid: GridContainer = %CardGrid
|
||||
@onready var card_scroll: ScrollContainer = $ScrollContainer
|
||||
@onready var song_list: SongStatsList = %SongList
|
||||
@onready var soundtracks_mode_button: Button = %SoundtracksModeButton
|
||||
@onready var songs_mode_button: Button = %SongsModeButton
|
||||
@onready var search_delay: Timer = %SearchDelay
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
search_field.text_changed.connect(func(_text: String) -> void: apply_filter())
|
||||
search_field.text_changed.connect(_on_search_text_changed)
|
||||
search_delay.timeout.connect(apply_filter)
|
||||
soundtracks_mode_button.pressed.connect(set_mode.bind(Mode.SOUNDTRACKS))
|
||||
songs_mode_button.pressed.connect(set_mode.bind(Mode.SONGS))
|
||||
song_list.song_pressed.connect(_on_song_pressed)
|
||||
clear_button.pressed.connect(clear_search)
|
||||
show_option.item_selected.connect(func(_index: int) -> void: apply_filter())
|
||||
sort_option.item_selected.connect(func(_index: int) -> void: apply_sort())
|
||||
@@ -53,14 +74,31 @@ func refresh() -> void:
|
||||
MusicServerClient.fetch_soundtrack_statistics(_on_loaded, _on_load_failed)
|
||||
|
||||
|
||||
## Shows every card again, keeping the show filter and the sort order.
|
||||
## Shows soundtracks or songs, keeping the search, the show filter and the sort order.
|
||||
func set_mode(new_mode: Mode) -> void:
|
||||
mode = new_mode
|
||||
soundtracks_mode_button.set_pressed_no_signal(mode == Mode.SOUNDTRACKS)
|
||||
songs_mode_button.set_pressed_no_signal(mode == Mode.SONGS)
|
||||
card_scroll.visible = mode == Mode.SOUNDTRACKS
|
||||
song_list.visible = mode == Mode.SONGS
|
||||
search_field.placeholder_text = "Search soundtracks" if mode == Mode.SOUNDTRACKS \
|
||||
else "Search songs and soundtracks"
|
||||
apply_sort()
|
||||
if mode == Mode.SOUNDTRACKS:
|
||||
apply_filter()
|
||||
|
||||
|
||||
## Shows everything again, keeping the show filter and the sort order.
|
||||
func clear_search() -> void:
|
||||
search_field.text = ""
|
||||
apply_filter()
|
||||
|
||||
|
||||
## Puts the cards in the order chosen in the sort menu.
|
||||
## Puts the cards or songs in the order chosen in the sort menu.
|
||||
func apply_sort() -> void:
|
||||
if mode == Mode.SONGS:
|
||||
apply_filter()
|
||||
return
|
||||
var order: SortOrder = sort_option.selected as SortOrder
|
||||
cards.sort_custom(func(a: SoundtrackCard, b: SoundtrackCard) -> bool:
|
||||
return _comes_before(a.soundtrack, b.soundtrack, order))
|
||||
@@ -68,8 +106,12 @@ func apply_sort() -> void:
|
||||
card_grid.move_child(cards[i], i)
|
||||
|
||||
|
||||
## Shows only the cards that match the search text and the show menu.
|
||||
## Shows only the cards or songs that match the search text and the show menu.
|
||||
func apply_filter() -> void:
|
||||
search_delay.stop()
|
||||
if mode == Mode.SONGS:
|
||||
_show_songs()
|
||||
return
|
||||
var search_text: String = search_field.text.strip_edges()
|
||||
var matching: Dictionary = {}
|
||||
for soundtrack_name: String in _search.find_matches(search_text) \
|
||||
@@ -100,12 +142,114 @@ func _on_loaded(soundtracks: Array[SoundtrackWithSongs]) -> void:
|
||||
while cards.size() > soundtracks.size():
|
||||
cards.pop_back().free()
|
||||
var names: Array[String] = []
|
||||
_soundtracks.clear()
|
||||
for i: int in soundtracks.size():
|
||||
cards[i].show_soundtrack(soundtracks[i])
|
||||
names.append(soundtracks[i].soundtrack_name)
|
||||
_soundtracks[soundtracks[i].soundtrack_name] = soundtracks[i]
|
||||
_search.set_names(names)
|
||||
_songs_ready = false
|
||||
apply_sort()
|
||||
apply_filter()
|
||||
if mode == Mode.SOUNDTRACKS:
|
||||
apply_filter()
|
||||
|
||||
|
||||
# Typing in the song list waits for a pause, since searching every song takes a moment
|
||||
func _on_search_text_changed(_text: String) -> void:
|
||||
if mode == Mode.SONGS:
|
||||
search_delay.start()
|
||||
else:
|
||||
apply_filter()
|
||||
|
||||
|
||||
func _on_song_pressed(song: SongInfoForStats) -> void:
|
||||
if _soundtracks.has(song.soundtrack_name):
|
||||
soundtrack_selected.emit(_soundtracks[song.soundtrack_name])
|
||||
|
||||
|
||||
# Shows the songs matching the search text and the show menu, in the sort menu's order
|
||||
func _show_songs() -> void:
|
||||
_prepare_songs()
|
||||
var search_text: String = search_field.text.strip_edges()
|
||||
var filter: ShowFilter = show_option.selected as ShowFilter
|
||||
var song_names: Dictionary = {}
|
||||
var soundtrack_names: Dictionary = {}
|
||||
if !search_text.is_empty():
|
||||
for song_name: String in _song_search.find_matches(search_text):
|
||||
song_names[song_name] = true
|
||||
for soundtrack_name: String in _search.find_matches(search_text):
|
||||
soundtrack_names[soundtrack_name] = true
|
||||
var shown: Array[SongInfoForStats] = []
|
||||
for song: SongInfoForStats in songs_in_order(_songs_a_to_z,
|
||||
sort_option.selected as SortOrder):
|
||||
if (search_text.is_empty() || song_names.has(song.song_name)
|
||||
|| soundtrack_names.has(song.soundtrack_name)) \
|
||||
&& _song_passes_filter(song, filter):
|
||||
shown.append(song)
|
||||
song_list.show_songs(shown, search_text)
|
||||
if shown.size() == _songs_a_to_z.size():
|
||||
count_label.text = "%d songs" % shown.size()
|
||||
else:
|
||||
count_label.text = "%d of %d songs" % [shown.size(), _songs_a_to_z.size()]
|
||||
|
||||
|
||||
# Lists every song once after loading, sorted A to Z, which the other orders start from
|
||||
func _prepare_songs() -> void:
|
||||
if _songs_ready:
|
||||
return
|
||||
_songs_ready = true
|
||||
_songs_a_to_z.clear()
|
||||
var names: Dictionary = {}
|
||||
for soundtrack: SoundtrackWithSongs in _soundtracks.values():
|
||||
for song: SongInfoForStats in soundtrack.songs:
|
||||
# The song list finds a song's soundtrack by its name
|
||||
song.soundtrack_name = soundtrack.soundtrack_name
|
||||
_songs_a_to_z.append(song)
|
||||
names[song.song_name] = true
|
||||
_songs_a_to_z.sort_custom(func(a: SongInfoForStats, b: SongInfoForStats) -> bool:
|
||||
var by_name: int = a.song_name.naturalnocasecmp_to(b.song_name)
|
||||
if by_name != 0:
|
||||
return by_name < 0
|
||||
return a.soundtrack_name.naturalnocasecmp_to(b.soundtrack_name) < 0)
|
||||
_song_search.set_names(names.keys())
|
||||
|
||||
|
||||
## [param a_to_z], songs sorted A to Z, in [param order]. Songs played as often stay in
|
||||
## A to Z order.
|
||||
static func songs_in_order(
|
||||
a_to_z: Array[SongInfoForStats], order: SortOrder) -> Array[SongInfoForStats]:
|
||||
match order:
|
||||
SortOrder.Z_TO_A:
|
||||
var z_to_a: Array[SongInfoForStats] = a_to_z.duplicate()
|
||||
z_to_a.reverse()
|
||||
return z_to_a
|
||||
SortOrder.MOST_PLAYED, SortOrder.LEAST_PLAYED:
|
||||
# Grouped by plays instead of sorted, which keeps the A to Z order within a
|
||||
# group and is quick for tens of thousands of songs
|
||||
var by_plays: Dictionary[int, Array] = {}
|
||||
for song: SongInfoForStats in a_to_z:
|
||||
if !by_plays.has(song.times_played):
|
||||
by_plays[song.times_played] = []
|
||||
by_plays[song.times_played].append(song)
|
||||
var plays: Array[int] = []
|
||||
plays.assign(by_plays.keys())
|
||||
plays.sort()
|
||||
if order == SortOrder.MOST_PLAYED:
|
||||
plays.reverse()
|
||||
var sorted: Array[SongInfoForStats] = []
|
||||
for times_played: int in plays:
|
||||
sorted.append_array(by_plays[times_played])
|
||||
return sorted
|
||||
return a_to_z
|
||||
|
||||
|
||||
static func _song_passes_filter(song: SongInfoForStats, filter: ShowFilter) -> bool:
|
||||
match filter:
|
||||
ShowFilter.PLAYED:
|
||||
return song.times_played > 0
|
||||
ShowFilter.NEVER_PLAYED:
|
||||
return song.times_played == 0
|
||||
return true
|
||||
|
||||
|
||||
func _on_load_failed() -> void:
|
||||
|
||||
Reference in new issue
Block a user