class_name SoundtrackCard extends PanelContainer ## One soundtrack in the statistics view: its name, how often and when it was last ## played, and its most and least played songs. Songs never played are not listed, and ## the least played are only shown when there are enough played songs to not repeat ## any. Clicking it asks for all its songs. ## ## Its cover shows on the left once [method load_cover] has fetched it; until then, and ## when the soundtrack has no cover, the soundtrack's initials stand in for it. ## Emitted when the card is clicked. signal pressed(soundtrack: SoundtrackWithSongs) ## How many of the most and least played songs the card lists. With fewer than twice ## as many played songs, all of them are listed as most played. const SONGS_SHOWN: int = 3 ## The soundtrack the card shows. var soundtrack: SoundtrackWithSongs # The cover time asked for, so a cover is only asked for again when it changed var _cover_asked: int = 0 @onready var cover_picture: TextureRect = %CoverPicture @onready var cover_placeholder: Label = %CoverPlaceholder @onready var name_label: Label = %NameLabel @onready var played_label: Label = %PlayedLabel @onready var last_played_label: Label = %LastPlayedLabel @onready var most_played_header: Label = %MostPlayedHeader @onready var most_played_list: VBoxContainer = %MostPlayedList @onready var least_played_header: Label = %LeastPlayedHeader @onready var least_played_list: VBoxContainer = %LeastPlayedList func _ready() -> void: mouse_entered.connect(_set_hovered.bind(true)) mouse_exited.connect(_set_hovered.bind(false)) gui_input.connect(_on_gui_input) ## Shows [param data] on the card. func show_soundtrack(data: SoundtrackWithSongs) -> void: var cover_changed: bool = soundtrack == null \ || soundtrack.soundtrack_uuid != data.soundtrack_uuid \ || soundtrack.cover_modified != data.cover_modified soundtrack = data if cover_changed: show_cover(null) _cover_asked = 0 cover_placeholder.text = initials(data.soundtrack_name) name_label.text = data.soundtrack_name tooltip_text = data.soundtrack_name played_label.text = played_text(data.soundtrack_played) last_played_label.text = last_played_text(data.soundtrack_last_played) last_played_label.visible = !data.soundtrack_last_played.is_empty() var most_played: Array[SongInfoForStats] = [] var least_played: Array[SongInfoForStats] = [] var played: Array[SongInfoForStats] = played_songs(data.songs) if played.size() >= SONGS_SHOWN * 2: most_played = played.slice(0, SONGS_SHOWN) least_played = songs_by_plays(played, true).slice(0, SONGS_SHOWN) else: # Too few to split without listing a song twice, so all of them are most played most_played = played most_played_header.text = song_list_header(data.songs.size(), played.size()) _fill_song_list(most_played_list, most_played) least_played_header.visible = !least_played.is_empty() _fill_song_list(least_played_list, least_played) ## Asks [param covers] for the soundtrack's cover, unless it already has, and shows it ## when it comes. Soundtracks without a cover keep their initials. func load_cover(covers: CoverCache) -> void: if soundtrack == null || soundtrack.cover_modified <= 0 \ || _cover_asked == soundtrack.cover_modified: return _cover_asked = soundtrack.cover_modified covers.fetch_cover(soundtrack.soundtrack_uuid, soundtrack.cover_modified, show_cover) ## Takes the soundtrack's new cover time [param modified], 0 when its cover was removed, ## and shows the initials until [method load_cover] fetches the new cover. func set_cover_time(modified: int) -> void: soundtrack.cover_modified = modified show_cover(null) _cover_asked = 0 ## Shows [param cover], or the initials when it is null. func show_cover(cover: Image) -> void: cover_picture.texture = ImageTexture.create_from_image(cover) if cover != null else null cover_placeholder.visible = cover == null ## Up to two initials of [param soundtrack_name], e.g. "FF" for "Final Fantasy VI". static func initials(soundtrack_name: String) -> String: var letters: String = "" for word: String in soundtrack_name.split(" ", false): var first: String = word.left(1) if first.to_upper() != first.to_lower(): letters += first.to_upper() if letters.length() == 2: break return letters if !letters.is_empty() else soundtrack_name.left(1) ## "Played 3 times", or "Never played" for [code]0[/code]. static func played_text(times_played: int) -> String: if times_played == 0: return "Never played" return "Played %d %s" % [times_played, "time" if times_played == 1 else "times"] ## "Last played 2026-10-01" from the server's date and time, or an empty String when ## there is none. static func last_played_text(last_played: String) -> String: if last_played.is_empty(): return "" return "Last played " + last_played.substr(0, 10) ## The songs of [param songs] that have been played, most played first. Songs never ## played are left out: the card only lists them in the popup with every song. static func played_songs(songs: Array[SongInfoForStats]) -> Array[SongInfoForStats]: var played: Array[SongInfoForStats] = [] played.assign(songs_by_plays(songs).filter( func(song: SongInfoForStats) -> bool: return song.times_played > 0)) return played ## The header above the most played songs, for a soundtrack with [param song_count] ## songs of which [param played_count] have been played. static func song_list_header(song_count: int, played_count: int) -> String: if song_count == 0: return "No songs" if played_count == 0: return "No songs played yet" return "Most played" ## One line of a song list: how often the song was played, then its name. static func song_text(song: SongInfoForStats) -> String: return "%d × %s" % [song.times_played, song.song_name] ## [param songs] sorted with the most played first, or the least played first with ## [param least_first], and by name when played as often. static func songs_by_plays( songs: Array[SongInfoForStats], least_first: bool = false) -> Array[SongInfoForStats]: var sorted: Array[SongInfoForStats] = songs.duplicate() sorted.sort_custom(func(a: SongInfoForStats, b: SongInfoForStats) -> bool: if a.times_played != b.times_played: return (a.times_played < b.times_played) == least_first return a.song_name.naturalnocasecmp_to(b.song_name) < 0 ) return sorted func _fill_song_list(list: VBoxContainer, songs: Array[SongInfoForStats]) -> void: while list.get_child_count() < songs.size(): var label: Label = Label.new() label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS list.add_child(label) for i: int in list.get_child_count(): var label: Label = list.get_child(i) label.visible = i < songs.size() if label.visible: label.text = song_text(songs[i]) func _set_hovered(hovered: bool) -> void: theme_type_variation = &"StatisticsCardHover" if hovered else &"StatisticsCard" func _on_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton && event.pressed \ && event.button_index == MOUSE_BUTTON_LEFT: accept_event() pressed.emit(soundtrack)