diff --git a/statistics/soundtrack_card.gd b/statistics/soundtrack_card.gd index f48dd4e..bfe5178 100644 --- a/statistics/soundtrack_card.gd +++ b/statistics/soundtrack_card.gd @@ -1,12 +1,15 @@ 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. Clicking it asks for all its songs. +## 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. ## Emitted when the card is clicked. signal pressed(soundtrack: SoundtrackWithSongs) -## How many of the most and least played songs the card lists. +## 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. @@ -36,13 +39,17 @@ func show_soundtrack(data: SoundtrackWithSongs) -> void: 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] = songs_by_plays(data.songs).slice(0, SONGS_SHOWN) - # Without the songs already shown as most played - var least_played: Array[SongInfoForStats] = songs_by_plays(data.songs, true).filter( - func(song: SongInfoForStats) -> bool: return !most_played.has(song) - ).slice(0, SONGS_SHOWN) + 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 = "Most played" if !data.songs.is_empty() else "No songs" + 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) @@ -63,6 +70,25 @@ static func last_played_text(last_played: String) -> String: 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] diff --git a/tests/integration/test_statistics_view.gd b/tests/integration/test_statistics_view.gd index 804f885..b36bff5 100644 --- a/tests/integration/test_statistics_view.gd +++ b/tests/integration/test_statistics_view.gd @@ -12,6 +12,7 @@ const STATISTICS: Array[Dictionary] = [ {"song_name": "To Far Away Times", "times_played": 0}, {"song_name": "Schala's Theme", "times_played": 4}, {"song_name": "Peaceful Days", "times_played": 1}, + {"song_name": "Zeal Palace", "times_played": 1}, ]}, {"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0, "songs": [{"song_name": "Underworld", "times_played": 0}]}, @@ -151,20 +152,48 @@ func test_card_shows_plays_last_played_and_most_and_least_played_songs() -> void assert_str(card.last_played_label.text).is_equal("Last played 2026-09-30") assert_array(_visible_texts(card.most_played_list)).contains_exactly( ["4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus"]) + # Songs never played are left out, so the least played are the least played of those assert_array(_visible_texts(card.least_played_list)).contains_exactly( - ["0 × To Far Away Times", "0 × Wind Scene", "1 × Corridors of Time"]) + ["1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace"]) + assert_bool(card.least_played_header.visible).is_true() -func test_card_of_never_played_soundtrack() -> void: +func test_card_of_never_played_soundtrack_lists_no_songs() -> void: await _open_statistics() var card: SoundtrackCard = _card("Terranigma") assert_str(card.played_label.text).is_equal("Never played") assert_bool(card.last_played_label.visible).is_false() - assert_array(_visible_texts(card.most_played_list)).contains_exactly(["0 × Underworld"]) + assert_str(card.most_played_header.text).is_equal("No songs played yet") + assert_array(_visible_texts(card.most_played_list)).is_empty() assert_bool(card.least_played_header.visible).is_false() +func test_card_with_one_played_song_lists_only_it() -> void: + await _open_statistics() + var card: SoundtrackCard = _card("Final Fantasy VI") + + assert_str(card.most_played_header.text).is_equal("Most played") + assert_array(_visible_texts(card.most_played_list)).contains_exactly(["1 × Dancing Mad"]) + assert_bool(card.least_played_header.visible).is_false() + assert_array(_visible_texts(card.least_played_list)).is_empty() + + +func test_card_with_fewer_than_six_played_songs_lists_them_all_as_most_played() -> void: + var songs: Array[Dictionary] = [{"song_name": "Never", "times_played": 0}] + for i: int in 5: + songs.append({"song_name": "Song %d" % (i + 1), "times_played": i + 1}) + _server.respond_json("/statistics/soundtracks", [ + {"soundtrack_name": "Lufia II", "soundtrack_played": 15, "songs": songs}]) + await _open_statistics() + var card: SoundtrackCard = _card("Lufia II") + + assert_array(_visible_texts(card.most_played_list)).contains_exactly( + ["5 × Song 5", "4 × Song 4", "3 × Song 3", "2 × Song 2", "1 × Song 1"]) + assert_bool(card.least_played_header.visible).is_false() + assert_array(_visible_texts(card.least_played_list)).is_empty() + + func test_card_without_songs() -> void: await _open_statistics() var card: SoundtrackCard = _card("earthbound") @@ -300,13 +329,14 @@ func test_clicking_a_card_shows_all_its_songs_most_played_first() -> void: assert_bool(popup.visible).is_true() assert_str(popup.title_label.text).is_equal("Chrono Trigger") assert_str(popup.summary_label.text) \ - .is_equal("Played 5 times · Last played 2026-09-30 · 7 songs") + .is_equal("Played 5 times · Last played 2026-09-30 · 8 songs") var rows: Array[String] = [] for row: ListRow in popup.song_list.get_children(): rows.append(row.text) assert_array(rows).contains_exactly([ "4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus", - "1 × Corridors of Time", "1 × Peaceful Days", "0 × To Far Away Times", "0 × Wind Scene", + "1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace", + "0 × To Far Away Times", "0 × Wind Scene", ])