Achievements: Show leaderboard times as relative

With a tooltip showing the absolute time.
This commit is contained in:
Stenzek
2026-01-05 13:18:11 +10:00
parent 4ab3669af0
commit fa39e63bbb
2 changed files with 189 additions and 33 deletions

View File

@@ -13,6 +13,7 @@
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/time_helpers.h"
#include "common/timer.h"
#include "IconsEmoji.h"
@@ -93,7 +94,8 @@ static void CloseLeaderboard();
static void DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard);
static void DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, u32 index, bool is_self,
float rank_column_width, float name_column_width, float time_column_width,
float column_spacing);
float column_spacing, std::time_t current_time, const std::tm& current_tm);
static SmallString FormatRelativeTimestamp(std::time_t timestamp, std::time_t current_time, const std::tm& current_tm);
namespace {
@@ -1692,6 +1694,14 @@ void FullscreenUI::DrawLeaderboardsWindow()
BeginMenuButtons(0, 0.0f, LAYOUT_MENU_BUTTON_X_PADDING, 8.0f, 0.0f, 4.0f);
ResetFocusHere();
// for drawing time popups
const std::time_t current_time = std::time(nullptr);
std::optional<std::tm> current_tm = Common::LocalTime(current_time);
if (!current_tm.has_value())
current_tm = std::tm{};
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f);
if (!s_achievements_locals.is_showing_all_leaderboard_entries)
{
if (s_achievements_locals.leaderboard_nearby_entries)
@@ -1700,7 +1710,8 @@ void FullscreenUI::DrawLeaderboardsWindow()
{
DrawLeaderboardEntry(s_achievements_locals.leaderboard_nearby_entries->entries[i], i,
static_cast<s32>(i) == s_achievements_locals.leaderboard_nearby_entries->user_index,
rank_column_width, name_column_width, time_column_width, column_spacing);
rank_column_width, name_column_width, time_column_width, column_spacing, current_time,
current_tm.value());
}
}
else
@@ -1720,7 +1731,8 @@ void FullscreenUI::DrawLeaderboardsWindow()
for (u32 i = 0; i < list->num_entries; i++)
{
DrawLeaderboardEntry(list->entries[i], i, static_cast<s32>(i) == list->user_index, rank_column_width,
name_column_width, time_column_width, column_spacing);
name_column_width, time_column_width, column_spacing, current_time,
current_tm.value());
}
}
@@ -1734,6 +1746,8 @@ void FullscreenUI::DrawLeaderboardsWindow()
}
}
ImGui::PopStyleVar(2);
EndMenuButtons();
}
EndFullscreenWindow();
@@ -1767,7 +1781,7 @@ void FullscreenUI::DrawLeaderboardsWindow()
void FullscreenUI::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, u32 index, bool is_self,
float rank_column_width, float name_column_width, float time_column_width,
float column_spacing)
float column_spacing, std::time_t current_time, const std::tm& current_tm)
{
ImRect bb;
bool visible, hovered;
@@ -1831,10 +1845,20 @@ void FullscreenUI::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& ent
const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
const std::string submit_time =
Host::FormatNumber(Host::NumberFormatType::LongDateTime, static_cast<s64>(entry.submitted));
const SmallString relative_time =
FormatRelativeTimestamp(static_cast<std::time_t>(entry.submitted), current_time, current_tm);
RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, font_weight, time_bb.Min, time_bb.Max,
text_color, submit_time, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &time_bb);
text_color, relative_time, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &time_bb);
if (time_bb.Contains(ImGui::GetIO().MousePos) && ImGui::BeginItemTooltip())
{
const std::string submit_time =
Host::FormatNumber(Host::NumberFormatType::LongDateTime, static_cast<s64>(entry.submitted));
ImGui::PushFont(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.NormalFontWeight);
ImGui::Text(ICON_EMOJI_CLOCK_FIVE_OCLOCK " %s", submit_time.c_str());
ImGui::PopFont();
ImGui::EndTooltip();
}
if (pressed)
{
@@ -1843,6 +1867,83 @@ void FullscreenUI::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& ent
Host::OpenURL(url);
}
}
SmallString FullscreenUI::FormatRelativeTimestamp(time_t timestamp, time_t current_time, const std::tm& current_tm)
{
const s64 diff = static_cast<s64>(current_time) - static_cast<s64>(timestamp);
constexpr s64 MINUTE = 60;
constexpr s64 HOUR = 60 * MINUTE;
constexpr s64 DAY = 24 * HOUR;
constexpr s64 WEEK = 7 * DAY;
if (diff < MINUTE)
return SmallString(TRANSLATE_SV("Achievements", "Just now"));
if (diff < HOUR)
{
const s64 minutes = diff / MINUTE;
return TRANSLATE_PLURAL_SSTR("Achievements", "%n minutes ago", "Relative time", static_cast<int>(minutes));
}
if (diff < DAY)
{
const s64 hours = diff / HOUR;
return TRANSLATE_PLURAL_SSTR("Achievements", "%n hours ago", "Relative time", static_cast<int>(hours));
}
if (diff < DAY * 2)
{
// Check if it's actually today vs yesterday
const std::optional<std::tm> timestamp_tm = Common::LocalTime(timestamp);
if (timestamp_tm.has_value() && timestamp_tm->tm_yday == current_tm.tm_yday &&
timestamp_tm->tm_year == current_tm.tm_year)
{
return SmallString(TRANSLATE_SV("Achievements", "Today"));
}
return SmallString(TRANSLATE_SV("Achievements", "Yesterday"));
}
if (diff < WEEK)
{
const s64 days = diff / DAY;
return TRANSLATE_PLURAL_SSTR("Achievements", "%n days ago", "Relative time", static_cast<int>(days));
}
const std::optional<std::tm> timestamp_tm = Common::LocalTime(timestamp);
if (!timestamp_tm.has_value())
return SmallString();
const int year_diff = current_tm.tm_year - timestamp_tm->tm_year;
const int month_diff = current_tm.tm_mon - timestamp_tm->tm_mon;
const int total_months = year_diff * 12 + month_diff;
if (total_months == 0)
{
// Less than a month - use weeks
const s64 weeks = diff / WEEK;
return TRANSLATE_PLURAL_SSTR("Achievements", "%n weeks ago", "Relative time", static_cast<int>(weeks));
}
if (total_months < 12)
return TRANSLATE_PLURAL_SSTR("Achievements", "%n months ago", "Relative time", total_months);
// For years, adjust if we haven't reached the anniversary yet
int years = year_diff;
if (current_tm.tm_mon < timestamp_tm->tm_mon ||
(current_tm.tm_mon == timestamp_tm->tm_mon && current_tm.tm_mday < timestamp_tm->tm_mday))
{
years--;
}
// Edge case: less than a full year but more than 11 months
if (years < 1)
return TRANSLATE_PLURAL_SSTR("Achievements", "%n months ago", "Relative time", total_months);
return TRANSLATE_PLURAL_SSTR("Achievements", "%n years ago", "Relative time", years);
}
void FullscreenUI::DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard)
{
SmallString title;

View File

@@ -4,8 +4,8 @@
<context>
<name>AchievementSettingsWidget</name>
<message numerus="yes">
<location filename="../achievementsettingswidget.cpp" line="194"/>
<location filename="../achievementsettingswidget.cpp" line="201"/>
<location filename="../achievementsettingswidget.cpp" line="197"/>
<location filename="../achievementsettingswidget.cpp" line="204"/>
<source>%n seconds</source>
<translation>
<numerusform>%n second</numerusform>
@@ -16,7 +16,7 @@
<context>
<name>Achievements</name>
<message numerus="yes">
<location filename="../../core/achievements.cpp" line="1272"/>
<location filename="../../core/achievements.cpp" line="1296"/>
<source>You have unlocked {} of %n achievements</source>
<comment>Achievement popup</comment>
<translation>
@@ -25,7 +25,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/achievements.cpp" line="1275"/>
<location filename="../../core/achievements.cpp" line="1299"/>
<source>and earned {} of %n points</source>
<comment>Achievement popup</comment>
<translation>
@@ -34,7 +34,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/achievements.cpp" line="1305"/>
<location filename="../../core/achievements.cpp" line="1329"/>
<source>%n achievements are not supported by DuckStation.</source>
<comment>Achievement popup</comment>
<translation>
@@ -43,8 +43,8 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/achievements.cpp" line="1379"/>
<location filename="../../core/achievements.cpp" line="1401"/>
<location filename="../../core/achievements.cpp" line="1403"/>
<location filename="../../core/achievements.cpp" line="1425"/>
<source>%n achievements</source>
<comment>Mastery popup</comment>
<translation>
@@ -53,9 +53,9 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/achievements.cpp" line="1381"/>
<location filename="../../core/achievements.cpp" line="1403"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="1161"/>
<location filename="../../core/achievements.cpp" line="1405"/>
<location filename="../../core/achievements.cpp" line="1427"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="1315"/>
<source>%n points</source>
<comment>Achievement points</comment>
<translation>
@@ -64,7 +64,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="354"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="358"/>
<source>%n unlocks have not been confirmed by the server.</source>
<comment>Pause Menu</comment>
<translation>
@@ -73,7 +73,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="888"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="919"/>
<source>You have unlocked all achievements and earned %n points!</source>
<comment>Point count</comment>
<translation>
@@ -82,7 +82,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="915"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="946"/>
<source>%n achievements are not supported by DuckStation and cannot be unlocked.</source>
<comment>Unsupported achievement count</comment>
<translation>
@@ -91,7 +91,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1405"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="1467"/>
<source>This game has %n leaderboards.</source>
<comment>Leaderboard count</comment>
<translation>
@@ -100,7 +100,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1407"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="1469"/>
<source>This subset has %n leaderboards.</source>
<comment>Leaderboard count</comment>
<translation>
@@ -108,11 +108,66 @@
<numerusform>This subset has %n leaderboards.</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1886"/>
<source>%n minutes ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n minute ago</numerusform>
<numerusform>%n minutes ago</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1892"/>
<source>%n hours ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n hour ago</numerusform>
<numerusform>%n hours ago</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1911"/>
<source>%n days ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n day ago</numerusform>
<numerusform>%n days ago</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1928"/>
<source>%n weeks ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n week ago</numerusform>
<numerusform>%n weeks ago</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1932"/>
<location filename="../../core/fullscreenui_achievements.cpp" line="1944"/>
<source>%n months ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n month ago</numerusform>
<numerusform>%n months ago</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_achievements.cpp" line="1946"/>
<source>%n years ago</source>
<comment>Relative time</comment>
<translation>
<numerusform>%n year ago</numerusform>
<numerusform>%n years ago</numerusform>
</translation>
</message>
</context>
<context>
<name>Cheats</name>
<message numerus="yes">
<location filename="../../core/cheats.cpp" line="1098"/>
<location filename="../../core/cheats.cpp" line="1118"/>
<source>%n game patches are active.</source>
<comment>OSD Message</comment>
<translation>
@@ -121,7 +176,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/cheats.cpp" line="1105"/>
<location filename="../../core/cheats.cpp" line="1125"/>
<source>%n cheats are enabled.</source>
<comment>OSD Message</comment>
<translation>
@@ -130,7 +185,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/cheats.cpp" line="1131"/>
<location filename="../../core/cheats.cpp" line="1151"/>
<source>%n cheats</source>
<comment>Cheats blocked by hardcore mode</comment>
<translation>
@@ -139,7 +194,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/cheats.cpp" line="1133"/>
<location filename="../../core/cheats.cpp" line="1153"/>
<source>%n patches</source>
<comment>Patches blocked by hardcore mode</comment>
<translation>
@@ -170,7 +225,7 @@
<context>
<name>FullscreenUI</name>
<message numerus="yes">
<location filename="../../core/fullscreenui_widgets.cpp" line="4597"/>
<location filename="../../core/fullscreenui_widgets.cpp" line="4740"/>
<source>%n seconds remaining</source>
<comment>Loading time</comment>
<translation>
@@ -179,7 +234,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/fullscreenui_widgets.cpp" line="4602"/>
<location filename="../../core/fullscreenui_widgets.cpp" line="4745"/>
<source>%n minutes remaining</source>
<comment>Loading time</comment>
<translation>
@@ -191,7 +246,7 @@
<context>
<name>GPU_HW</name>
<message numerus="yes">
<location filename="../../core/gpu_hw_texture_cache.cpp" line="3710"/>
<location filename="../../core/gpu_hw_texture_cache.cpp" line="3708"/>
<source>%n replacement textures found.</source>
<comment>Replacement texture count</comment>
<translation>
@@ -203,7 +258,7 @@
<context>
<name>GameList</name>
<message numerus="yes">
<location filename="../../core/game_list.cpp" line="1646"/>
<location filename="../../core/game_list.cpp" line="1648"/>
<source>%n seconds</source>
<translation>
<numerusform>%n second</numerusform>
@@ -211,7 +266,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/game_list.cpp" line="1642"/>
<location filename="../../core/game_list.cpp" line="1644"/>
<source>%n hours</source>
<translation>
<numerusform>%n hour</numerusform>
@@ -219,7 +274,7 @@
</translation>
</message>
<message numerus="yes">
<location filename="../../core/game_list.cpp" line="1644"/>
<location filename="../../core/game_list.cpp" line="1646"/>
<source>%n minutes</source>
<translation>
<numerusform>%n minute</numerusform>
@@ -230,7 +285,7 @@
<context>
<name>InputBindingWidget</name>
<message numerus="yes">
<location filename="../inputbindingwidgets.cpp" line="101"/>
<location filename="../inputbindingwidgets.cpp" line="102"/>
<source>%n bindings</source>
<translation>
<numerusform>%n binding</numerusform>