|
| 1 | +using System.Globalization; |
| 2 | +using Discord; |
| 3 | +using RustPlusBot.Abstractions.Time; |
| 4 | +using RustPlusBot.Domain.Alarms; |
| 5 | + |
| 6 | +namespace RustPlusBot.Features.Alarms.Rendering; |
| 7 | + |
| 8 | +/// <summary>Renders a Smart Alarm as a Discord embed + control row, and the pairing-prompt embed + row. Pure.</summary> |
| 9 | +/// <param name="localizer">The alarm localizer.</param> |
| 10 | +/// <param name="clock">The clock used to compute relative trigger times.</param> |
| 11 | +internal sealed class AlarmEmbedRenderer(AlarmLocalizer localizer, IClock clock) |
| 12 | +{ |
| 13 | + /// <summary>Renders the alarm embed and its control buttons.</summary> |
| 14 | + /// <param name="alarm">The alarm to render.</param> |
| 15 | + /// <param name="unreachable">When true the alarm entity is currently unreachable.</param> |
| 16 | + /// <param name="culture">The guild culture (BCP-47 primary tag).</param> |
| 17 | + /// <returns>The embed and the component rows.</returns> |
| 18 | + public (Embed Embed, MessageComponent Components) RenderAlarm(SmartAlarm alarm, bool unreachable, string culture) |
| 19 | + { |
| 20 | + ArgumentNullException.ThrowIfNull(alarm); |
| 21 | + |
| 22 | + string statusKey; |
| 23 | + if (unreachable) |
| 24 | + { |
| 25 | + statusKey = "alarm.status.unreachable"; |
| 26 | + } |
| 27 | + else if (alarm.LastIsActive) |
| 28 | + { |
| 29 | + statusKey = "alarm.status.active"; |
| 30 | + } |
| 31 | + else |
| 32 | + { |
| 33 | + statusKey = "alarm.status.armed"; |
| 34 | + } |
| 35 | + |
| 36 | + var triggered = alarm.LastTriggeredUtc is { } t |
| 37 | + ? localizer.Get("alarm.embed.lasttriggered", culture, CompactDuration(clock.UtcNow - t)) |
| 38 | + : localizer.Get("alarm.embed.nevertriggered", culture); |
| 39 | + |
| 40 | + var embed = new EmbedBuilder() |
| 41 | + .WithTitle(alarm.Name) |
| 42 | + .WithDescription($"{localizer.Get(statusKey, culture)}\n{triggered}") |
| 43 | + .WithFooter(localizer.Get("alarm.embed.footer", culture, alarm.EntityId)) |
| 44 | + .Build(); |
| 45 | + |
| 46 | + var tail = $"{alarm.ServerId}:{alarm.EntityId}"; |
| 47 | + var pingKey = alarm.PingEveryone ? "alarm.button.ping.on" : "alarm.button.ping.off"; |
| 48 | + var relayKey = alarm.RelayToTeamChat ? "alarm.button.relay.on" : "alarm.button.relay.off"; |
| 49 | + var components = new ComponentBuilder() |
| 50 | + .WithButton(localizer.Get(pingKey, culture), AlarmComponentIds.PingTogglePrefix + tail, |
| 51 | + alarm.PingEveryone ? ButtonStyle.Success : ButtonStyle.Secondary, disabled: unreachable) |
| 52 | + .WithButton(localizer.Get(relayKey, culture), AlarmComponentIds.RelayTogglePrefix + tail, |
| 53 | + alarm.RelayToTeamChat ? ButtonStyle.Success : ButtonStyle.Secondary, disabled: unreachable) |
| 54 | + .WithButton(localizer.Get("alarm.button.rename", culture), AlarmComponentIds.RenamePrefix + tail, |
| 55 | + ButtonStyle.Secondary, disabled: unreachable) |
| 56 | + .Build(); |
| 57 | + |
| 58 | + return (embed, components); |
| 59 | + } |
| 60 | + |
| 61 | + /// <summary>Renders the transient "New alarm detected — Add it?" prompt.</summary> |
| 62 | + /// <param name="serverId">The server id.</param> |
| 63 | + /// <param name="entityId">The entity id.</param> |
| 64 | + /// <param name="defaultName">The generated default name.</param> |
| 65 | + /// <param name="culture">The guild culture (BCP-47 primary tag).</param> |
| 66 | + /// <returns>The prompt embed and Accept/Dismiss row.</returns> |
| 67 | + public (Embed Embed, MessageComponent Components) RenderPrompt( |
| 68 | + Guid serverId, |
| 69 | + ulong entityId, |
| 70 | + string defaultName, |
| 71 | + string culture) |
| 72 | + { |
| 73 | + var embed = new EmbedBuilder() |
| 74 | + .WithTitle(localizer.Get("alarm.prompt.title", culture)) |
| 75 | + .WithDescription(localizer.Get("alarm.prompt.body", culture, defaultName)) |
| 76 | + .Build(); |
| 77 | + |
| 78 | + var tail = $"{serverId}:{entityId}"; |
| 79 | + var components = new ComponentBuilder() |
| 80 | + .WithButton(localizer.Get("alarm.prompt.accept", culture), AlarmComponentIds.AcceptPrefix + tail, |
| 81 | + ButtonStyle.Success) |
| 82 | + .WithButton(localizer.Get("alarm.prompt.dismiss", culture), AlarmComponentIds.DismissPrefix + tail, |
| 83 | + ButtonStyle.Secondary) |
| 84 | + .Build(); |
| 85 | + |
| 86 | + return (embed, components); |
| 87 | + } |
| 88 | + |
| 89 | + /// <summary>Formats a duration compactly: "5m", "2h 10m", "3d 4h", "<1m".</summary> |
| 90 | + /// <param name="span">The duration to format.</param> |
| 91 | + /// <returns>A compact human-readable duration string.</returns> |
| 92 | + private static string CompactDuration(TimeSpan span) |
| 93 | + { |
| 94 | + if (span.TotalDays >= 1) |
| 95 | + { |
| 96 | + return string.Create(CultureInfo.InvariantCulture, $"{(int)span.TotalDays}d {span.Hours}h"); |
| 97 | + } |
| 98 | + |
| 99 | + if (span.TotalHours >= 1) |
| 100 | + { |
| 101 | + return string.Create(CultureInfo.InvariantCulture, $"{(int)span.TotalHours}h {span.Minutes}m"); |
| 102 | + } |
| 103 | + |
| 104 | + if (span.TotalMinutes >= 1) |
| 105 | + { |
| 106 | + return string.Create(CultureInfo.InvariantCulture, $"{(int)span.TotalMinutes}m"); |
| 107 | + } |
| 108 | + |
| 109 | + return "<1m"; |
| 110 | + } |
| 111 | +} |
0 commit comments