diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs
index 5fdb909..557e8cb 100755
--- a/HellionChat/Configuration.cs
+++ b/HellionChat/Configuration.cs
@@ -603,6 +603,20 @@ public class Tab
}
}
+ ///
+ /// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
+ /// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
+ ///
+ public int Count
+ {
+ get
+ {
+ LockSlim.Wait(-1);
+ try { return Messages.Count; }
+ finally { LockSlim.Release(); }
+ }
+ }
+
///
/// Returns an array copy of the message list for usage outside of main thread
///
diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs
index 784a394..bec7fff 100644
--- a/HellionChat/Resources/HellionStrings.Designer.cs
+++ b/HellionChat/Resources/HellionStrings.Designer.cs
@@ -315,4 +315,8 @@ internal class HellionStrings
internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle));
internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody));
internal static string ChatTwoConflictAction => Get(nameof(ChatTwoConflictAction));
+
+ // Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels
+ internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled));
+ internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open));
}
diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx
index 6957f51..5adeb67 100644
--- a/HellionChat/Resources/HellionStrings.de.resx
+++ b/HellionChat/Resources/HellionStrings.de.resx
@@ -716,4 +716,10 @@
Behalten
+
+ Privacy-First
+
+
+ Offen
+
diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx
index 67a1f16..86d829d 100644
--- a/HellionChat/Resources/HellionStrings.resx
+++ b/HellionChat/Resources/HellionStrings.resx
@@ -716,4 +716,10 @@
Keep current
+
+ Privacy-First
+
+
+ Open
+
diff --git a/HellionChat/Ui/StatusBar.cs b/HellionChat/Ui/StatusBar.cs
new file mode 100644
index 0000000..77a6811
--- /dev/null
+++ b/HellionChat/Ui/StatusBar.cs
@@ -0,0 +1,171 @@
+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 (v1.2.0). Fix 22 px hoch, BorderTop als Trenner.
+/// Slots links → rechts: Channel-Indicator (Color-Dot + Channel-Name),
+/// Privacy-Badge (Lock-Icon + Privacy-Label), Counts (Tabs + Msgs),
+/// Tells (Auto-Tell-Counter, hidden bei 0), Version (rechtsbündig, muted).
+///
+/// Update-Frequenz: 1×/Sekunde. Format-Strings werden zwischen Updates
+/// gecached, damit kein Per-Frame-Format-Allocation entsteht.
+///
+internal sealed class StatusBar
+{
+ public const float Height = 22f;
+ private const long UpdateIntervalMs = 1000;
+
+ // Cache-State — initial outdated, damit der erste Frame frisch berechnet.
+ private long _lastUpdateMs = -UpdateIntervalMs;
+ private string _cachedCountsText = string.Empty;
+ private string _cachedTellsText = string.Empty;
+
+ ///
+ /// Reine String-Logik — testbar ohne ImGui-Init.
+ ///
+ public static string FormatCounts(int tabs, int messages)
+ {
+ // InvariantCulture: User-System-Locale darf das Format nicht
+ // verändern (de_DE würde sonst "1,2k" statt "1.2k" liefern).
+ 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}";
+ }
+
+ ///
+ /// Reine String-Logik — testbar ohne ImGui-Init.
+ /// 0 Tells → Leerstring (Slot wird ausgeblendet).
+ ///
+ public static string FormatTells(int count)
+ {
+ if (count <= 0) return string.Empty;
+ return $"{count} {(count == 1 ? "tell" : "tells")}";
+ }
+
+ ///
+ /// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren.
+ /// Nicht für Production-Render.
+ ///
+ 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;
+ }
+
+ ///
+ /// Render-Pfad. Aufrufer pusht bereits den HellionStyle/Theme;
+ /// wir lesen nur die aktiven Theme-Farben und zeichnen.
+ ///
+ public void Draw(Plugin plugin)
+ {
+ var theme = plugin.ThemeRegistry.Active;
+ var now = Environment.TickCount64;
+
+ // Counts pro Frame berechnen ist günstig (List<>.Count, kleine
+ // Sums); Format-String wird gecached.
+ var tabs = Plugin.Config.Tabs.Count;
+ var messages = Plugin.Config.Tabs.Sum(t => t.Messages.Count);
+ var tells = Plugin.Config.Tabs.Count(t => t.IsTempTab);
+ UpdateCacheIfDue(now, tabs, messages, tells);
+
+ // BorderTop als Trenner — DrawList-Line, ImGui-Separator hat zu viel 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)); // BorderTop-Spacing
+
+ using var group = ImRaii.Group();
+
+ // 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 — abgeleitet aus PrivacyFilterEnabled.
+ 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 (nur wenn > 0)
+ if (!string.IsNullOrEmpty(_cachedTellsText))
+ {
+ ImGui.SameLine();
+ DrawSeparator();
+ ImGui.SameLine();
+ ImGui.TextUnformatted(_cachedTellsText);
+ }
+
+ // Slot 5: Version (rechtsbündig, muted)
+ var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
+ var versionWidth = ImGui.CalcTextSize(versionText).X;
+ var rightCursor = ImGui.GetWindowSize().X - versionWidth - ImGui.GetStyle().WindowPadding.X;
+ ImGui.SameLine(rightCursor);
+ 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("·");
+ }
+}