c4c85cf4b8
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility. - Standardized internal code documentation by converting XML-doc blocks to standard comment format. - Cleaned up inline comments and removed redundant versioning metadata across the codebase. - Refactored non-functional text elements to improve readability and maintain a consistent style.
177 lines
5.9 KiB
C#
177 lines
5.9 KiB
C#
using System.Globalization;
|
|
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using HellionChat.Code;
|
|
using HellionChat.Resources;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui;
|
|
|
|
// Bottom status bar, 22px tall. Slots left to right: channel indicator,
|
|
// privacy badge, counts, tells (hidden at 0), version (right-aligned).
|
|
// Updates at 1Hz; format strings are cached between updates.
|
|
internal sealed class StatusBar
|
|
{
|
|
public const float Height = 22f;
|
|
private const long UpdateIntervalMs = 1000;
|
|
|
|
// Initially outdated so the first frame always computes fresh.
|
|
private long _lastUpdateMs = -UpdateIntervalMs;
|
|
private string _cachedCountsText = string.Empty;
|
|
private string _cachedTellsText = string.Empty;
|
|
|
|
// Pure string logic, testable without ImGui init.
|
|
public static string FormatCounts(int tabs, int messages)
|
|
{
|
|
// InvariantCulture so locale doesn't affect the format (e.g. de_DE "1,2k").
|
|
var msgPart =
|
|
messages >= 1000
|
|
? string.Format(CultureInfo.InvariantCulture, "{0:0.0}k msg", messages / 1000.0)
|
|
: $"{messages} msg";
|
|
var tabsPart = $"{tabs} {(tabs == 1 ? "tab" : "tabs")}";
|
|
return $"{tabsPart} · {msgPart}";
|
|
}
|
|
|
|
// Pure string logic, testable without ImGui init. Returns empty string at 0 tells.
|
|
public static string FormatTells(int count)
|
|
{
|
|
if (count <= 0)
|
|
return string.Empty;
|
|
return $"{count} {(count == 1 ? "tell" : "tells")}";
|
|
}
|
|
|
|
// Single-pass replacement for a LINQ Sum+Count pair. Pure helper for unit testing.
|
|
internal static (int messages, int tells) AggregateForStatusBar(IList<Tab> tabs)
|
|
{
|
|
int messages = 0,
|
|
tells = 0;
|
|
foreach (var t in tabs)
|
|
{
|
|
messages += t.Messages.Count;
|
|
if (t.IsTempTab)
|
|
tells++;
|
|
}
|
|
return (messages, tells);
|
|
}
|
|
|
|
// Test hook to verify cache logic without a real time source.
|
|
internal (string counts, string tells) SnapshotForTest(
|
|
long now,
|
|
int tabs,
|
|
int messages,
|
|
int tells
|
|
)
|
|
{
|
|
UpdateCacheIfDue(now, tabs, messages, tells);
|
|
return (_cachedCountsText, _cachedTellsText);
|
|
}
|
|
|
|
private void UpdateCacheIfDue(long now, int tabs, int messages, int tells)
|
|
{
|
|
if (now - _lastUpdateMs < UpdateIntervalMs)
|
|
return;
|
|
_cachedCountsText = FormatCounts(tabs, messages);
|
|
_cachedTellsText = FormatTells(tells);
|
|
_lastUpdateMs = now;
|
|
}
|
|
|
|
public void Draw(Plugin plugin)
|
|
{
|
|
var theme = plugin.ThemeRegistry.Active;
|
|
var now = Environment.TickCount64;
|
|
|
|
if (now - _lastUpdateMs >= UpdateIntervalMs)
|
|
{
|
|
var (messages, tells) = AggregateForStatusBar(Plugin.Config.Tabs);
|
|
UpdateCacheIfDue(now, Plugin.Config.Tabs.Count, messages, tells);
|
|
}
|
|
|
|
// Border top via DrawList -- ImGui.Separator has too much padding.
|
|
var cursorY = ImGui.GetCursorScreenPos().Y;
|
|
var winLeft = ImGui.GetWindowPos().X;
|
|
var winRight = winLeft + ImGui.GetWindowSize().X;
|
|
ImGui
|
|
.GetWindowDrawList()
|
|
.AddLine(
|
|
new Vector2(winLeft, cursorY),
|
|
new Vector2(winRight, cursorY),
|
|
ColourUtil.RgbaToAbgr(theme.Colors.Border),
|
|
1f
|
|
);
|
|
|
|
ImGui.Dummy(new Vector2(0, 2));
|
|
|
|
// Slot 1: active channel indicator
|
|
var inputCh = plugin.CurrentTab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
|
|
var hasChannel = inputCh != InputChannel.Invalid;
|
|
var chatType = inputCh.ToChatType();
|
|
var channelName = hasChannel ? chatType.Name() : "—";
|
|
var channelColor = hasChannel
|
|
? (plugin.Functions.Chat.GetChannelColor(chatType) ?? theme.Colors.TextMuted)
|
|
: theme.Colors.TextMuted;
|
|
DrawDot(channelColor);
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted(channelName);
|
|
|
|
// Slot 2: privacy badge
|
|
ImGui.SameLine();
|
|
DrawSeparator();
|
|
ImGui.SameLine();
|
|
using (plugin.FontManager.FontAwesome.Push())
|
|
{
|
|
ImGui.TextUnformatted(FontAwesomeIcon.Lock.ToIconString());
|
|
}
|
|
ImGui.SameLine();
|
|
var privacyLabel = Plugin.Config.PrivacyFilterEnabled
|
|
? HellionStrings.StatusBar_Privacy_Enabled
|
|
: HellionStrings.StatusBar_Privacy_Open;
|
|
ImGui.TextUnformatted(privacyLabel);
|
|
|
|
// Slot 3: counts
|
|
ImGui.SameLine();
|
|
DrawSeparator();
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted(_cachedCountsText);
|
|
|
|
// Slot 4: tells (hidden at 0)
|
|
if (!string.IsNullOrEmpty(_cachedTellsText))
|
|
{
|
|
ImGui.SameLine();
|
|
DrawSeparator();
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted(_cachedTellsText);
|
|
}
|
|
|
|
// Slot 5: version, right-aligned, muted
|
|
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
|
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
|
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
|
ImGui.SameLine(contentRegionMax - versionWidth);
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
|
{
|
|
ImGui.TextUnformatted(versionText);
|
|
}
|
|
}
|
|
|
|
private static void DrawDot(uint rgba)
|
|
{
|
|
var pos = ImGui.GetCursorScreenPos();
|
|
const float radius = 4f;
|
|
ImGui
|
|
.GetWindowDrawList()
|
|
.AddCircleFilled(
|
|
new Vector2(pos.X + radius, pos.Y + ImGui.GetTextLineHeight() / 2f),
|
|
radius,
|
|
ColourUtil.RgbaToAbgr(rgba)
|
|
);
|
|
ImGui.Dummy(new Vector2(radius * 2 + 4, ImGui.GetTextLineHeight()));
|
|
}
|
|
|
|
private static void DrawSeparator()
|
|
{
|
|
ImGui.TextDisabled("·");
|
|
}
|
|
}
|