diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index 41029f551..d855447c8 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -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 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(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(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(entry.submitted)); + const SmallString relative_time = + FormatRelativeTimestamp(static_cast(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(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(current_time) - static_cast(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(minutes)); + } + + if (diff < DAY) + { + const s64 hours = diff / HOUR; + return TRANSLATE_PLURAL_SSTR("Achievements", "%n hours ago", "Relative time", static_cast(hours)); + } + + if (diff < DAY * 2) + { + // Check if it's actually today vs yesterday + const std::optional 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(days)); + } + + const std::optional 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(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; diff --git a/src/duckstation-qt/translations/duckstation-qt_en.ts b/src/duckstation-qt/translations/duckstation-qt_en.ts index acff72bb3..7b48380b4 100644 --- a/src/duckstation-qt/translations/duckstation-qt_en.ts +++ b/src/duckstation-qt/translations/duckstation-qt_en.ts @@ -4,8 +4,8 @@ AchievementSettingsWidget - - + + %n seconds %n second @@ -16,7 +16,7 @@ Achievements - + You have unlocked {} of %n achievements Achievement popup @@ -25,7 +25,7 @@ - + and earned {} of %n points Achievement popup @@ -34,7 +34,7 @@ - + %n achievements are not supported by DuckStation. Achievement popup @@ -43,8 +43,8 @@ - - + + %n achievements Mastery popup @@ -53,9 +53,9 @@ - - - + + + %n points Achievement points @@ -64,7 +64,7 @@ - + %n unlocks have not been confirmed by the server. Pause Menu @@ -73,7 +73,7 @@ - + You have unlocked all achievements and earned %n points! Point count @@ -82,7 +82,7 @@ - + %n achievements are not supported by DuckStation and cannot be unlocked. Unsupported achievement count @@ -91,7 +91,7 @@ - + This game has %n leaderboards. Leaderboard count @@ -100,7 +100,7 @@ - + This subset has %n leaderboards. Leaderboard count @@ -108,11 +108,66 @@ This subset has %n leaderboards. + + + %n minutes ago + Relative time + + %n minute ago + %n minutes ago + + + + + %n hours ago + Relative time + + %n hour ago + %n hours ago + + + + + %n days ago + Relative time + + %n day ago + %n days ago + + + + + %n weeks ago + Relative time + + %n week ago + %n weeks ago + + + + + + %n months ago + Relative time + + %n month ago + %n months ago + + + + + %n years ago + Relative time + + %n year ago + %n years ago + + Cheats - + %n game patches are active. OSD Message @@ -121,7 +176,7 @@ - + %n cheats are enabled. OSD Message @@ -130,7 +185,7 @@ - + %n cheats Cheats blocked by hardcore mode @@ -139,7 +194,7 @@ - + %n patches Patches blocked by hardcore mode @@ -170,7 +225,7 @@ FullscreenUI - + %n seconds remaining Loading time @@ -179,7 +234,7 @@ - + %n minutes remaining Loading time @@ -191,7 +246,7 @@ GPU_HW - + %n replacement textures found. Replacement texture count @@ -203,7 +258,7 @@ GameList - + %n seconds %n second @@ -211,7 +266,7 @@ - + %n hours %n hour @@ -219,7 +274,7 @@ - + %n minutes %n minute @@ -230,7 +285,7 @@ InputBindingWidget - + %n bindings %n binding