merge: v1.2.0 Layout Refresh

27 commits brought in from feature/v1.2.0-layout-refresh:
- Sidebar/Top-Tabs visual modernisation (icon-only sidebar with
  44px fixed width and tooltip, vertical accent pill, top-tab
  underline pill).
- TabIconMapping with single-source 15-glyph pool, per-tab
  Icon override via Settings → Tabs combobox.
- AutoTellTabTint hash-based icon+color differentiation
  (84 distinct combinations) for parallel tells.
- Bottom status bar (22px): channel/privacy/counts/tells/version.
- Card-Rows as default message render with Compact-Density
  opt-out toggle.
- Pulsing red unread-dot indicator on sidebar tab icons,
  respects Configuration.ReduceMotion.
- Migration v14 → v15: legacy theme fields removed, Appearance
  bindings cleaned to use Themes tab as single source.
- Settings-Save chat-history preservation: UpdateFrom Identifier-
  mapping for persistent tabs, TempTab skip in ClearAllTabs/
  FilterAllTabs, conditional refilter only for filter-relevant
  changes.
- Hellion font (Exo 2) no longer blocks FontSizeV2 adjustment —
  4K user can scale up the variable font.

Tag v1.2.0 sits on the last feature commit (3da550c).
Forge-Auto-Announce-Action triggers on tag push.
This commit is contained in:
2026-05-06 00:18:37 +02:00
20 changed files with 1028 additions and 110 deletions
+17
View File
@@ -0,0 +1,17 @@
---
subtitle: "Layout Refresh"
versionsnatur: "Major-UI-Cycle"
---
- Sidebar im modernisiertem Layout: nur noch Icons in fixer 44 px Breite, Tab-Name als Tooltip beim Hover, vertikale Akzent-Pill markiert den aktiven Tab
- Top-Tabs bekommen eine 2 px Akzent-Underline am unteren Rand statt Background-Fill für den aktiven Tab
- Pro Tab eigenes Icon zuweisbar via Settings → Tabs (15 FontAwesome-Glyphen-Pool)
- Bottom-Status-Bar (22 px) zeigt fünf Live-Signale: aktiver Channel mit Color-Dot, Privacy-Badge, Tab- und Message-Counter, Auto-Tell-Counter, Plugin-Version. Update einmal pro Sekunde, gecached
- Card-Rows als Default-Layout für Messages: Sender-Header in Channel-Farbe, Body auf eigener Zeile, dezenter Trenner zwischen den Karten
- `Compact Density`-Toggle in Aussehen schaltet zurück auf den klassischen Einzeiler `[HH:mm] Sender: Text`
- Auto-Tell-Tabs unterscheiden sich jetzt visuell: jeder Tell-Partner bekommt ein eigenes Icon (envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84 Icon-Farb-Kombinationen, gleicher Partner ergibt konsistent dieselbe Kombination
- Pulsierender roter Dot oben rechts am Sidebar-Icon wenn ein Tab ungelesene Nachrichten hat. Sanft, 2-Sekunden-Cycle, lässt sich über `Configuration.ReduceMotion` deaktivieren (UI-Toggle kommt in v1.3.0)
- Migration v14 → v15: alte `HellionThemeEnabled` und `HellionThemeWindowOpacity` Konfigurationsfelder entfernt, alle anderen Settings bleiben erhalten
- Bug-Fix: Settings speichern zerstört nicht mehr den Chat-Verlauf. Der schwere Refilter-Cycle läuft jetzt nur noch wenn sich Filter-relevante Settings tatsächlich geändert haben (Privacy-Filter, gemerkte Channels, Tab-Channel-Auswahl) — Cosmetic-Änderungen wie Theme oder Tab-Icons lassen den Chat unverändert. Persistente Tabs und Auto-Tell-Tabs überleben beide
- Bug-Fix: Sidebar-Buttons sitzen jetzt vertikal in einer Linie mit der ersten Message-Zeile, Status-Bar-Versionsname wird vollständig angezeigt
Animation-Polish (Lerps, Theme-Crossfade, Header-Quick-Picker) folgt in v1.3.0. v1.2.0 ist bewusst Hard-Switch — sauberes Layout zuerst, Bewegung später.
+46 -21
View File
@@ -34,7 +34,7 @@ public class ConfigKeyBind
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
private const int LatestVersion = 14; private const int LatestVersion = 15;
public int Version { get; set; } = LatestVersion; public int Version { get; set; } = LatestVersion;
@@ -80,19 +80,6 @@ public class Configuration : IPluginConfiguration
// ChatTwo users skip it because the v6→v7 migration sets the flag. // ChatTwo users skip it because the v6→v7 migration sets the flag.
public bool FirstRunCompleted; public bool FirstRunCompleted;
// Hellion Chat global ImGui theme — applied to every plugin window in
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
// can flip this off in the Privacy tab.
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
public bool HellionThemeEnabled = true;
// Window background opacity, 0.51.0. Lower values make the plugin
// panes more glass-like so the game shines through. Default 0.5
// matches the maintainer's daily-driver preference; users who want
// a less translucent look bump it up in Aussehen → Theme.
[Obsolete("Replaced by WindowOpacity in v14")]
public float HellionThemeWindowOpacity = 0.5f;
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
// instead of whatever GlobalFontV2.FontId points at. Default ON so a // instead of whatever GlobalFontV2.FontId points at. Default ON so a
// fresh install gets the Hellion typography out-of-the-box; flip OFF // fresh install gets the Hellion typography out-of-the-box; flip OFF
@@ -315,10 +302,33 @@ public class Configuration : IPluginConfiguration
// never present in a disk-loaded copy. Keep the live temp tabs of // never present in a disk-loaded copy. Keep the live temp tabs of
// *this* configuration alive across an UpdateFrom so a settings // *this* configuration alive across an UpdateFrom so a settings
// save (or sidebar-mode toggle) does not silently destroy the // save (or sidebar-mode toggle) does not silently destroy the
// user's open tell conversations. Persistent tabs from `other` // user's open tell conversations.
// still get the regular clone-replace treatment. //
// For persistent tabs we go through Tab.Clone() which intentionally
// does NOT copy the NonSerialized Messages list (avoids shared
// mutable state on disk-load). On a settings save that means the
// chat history for every persistent tab would be wiped — bug
// reported by Flo 2026-05-05. We work around it by capturing the
// live MessageList (and LastSendUnread counter) by Identifier
// before the replace, then restoring it onto the freshly cloned
// tabs whose Identifier survives Tab.Clone(). New tabs added in
// settings get a fresh empty MessageList; deleted tabs lose their
// history (intended).
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList(); var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList(); var livePersistentSession = Tabs
.Where(t => !t.IsTempTab)
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t =>
{
var clone = t.Clone();
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
{
clone.Messages = live.Messages;
clone.LastSendUnread = live.LastSendUnread;
}
return clone;
}).ToList();
Tabs.AddRange(liveTempTabs); Tabs.AddRange(liveTempTabs);
OverrideStyle = other.OverrideStyle; OverrideStyle = other.OverrideStyle;
@@ -336,10 +346,6 @@ public class Configuration : IPluginConfiguration
RetentionLastRunAt = other.RetentionLastRunAt; RetentionLastRunAt = other.RetentionLastRunAt;
FirstRunCompleted = other.FirstRunCompleted; FirstRunCompleted = other.FirstRunCompleted;
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
HellionThemeEnabled = other.HellionThemeEnabled;
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
#pragma warning restore CS0612, CS0618
UseHellionFont = other.UseHellionFont; UseHellionFont = other.UseHellionFont;
// v1.1.0 theme engine fields // v1.1.0 theme engine fields
@@ -394,6 +400,11 @@ public class Tab
{ {
public string Name = Language.Tab_DefaultName; public string Name = Language.Tab_DefaultName;
// v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
// Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
// User können hier per Settings → Tabs einen eigenen Glyph setzen.
public string? Icon = null;
[Obsolete("Removed in favor of SelectedChannels")] [Obsolete("Removed in favor of SelectedChannels")]
public Dictionary<ChatType, ChatSource> ChatCodes = new(); public Dictionary<ChatType, ChatSource> ChatCodes = new();
@@ -598,6 +609,20 @@ public class Tab
} }
} }
/// <summary>
/// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
/// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
/// </summary>
public int Count
{
get
{
LockSlim.Wait(-1);
try { return Messages.Count; }
finally { LockSlim.Release(); }
}
}
/// <summary> /// <summary>
/// Returns an array copy of the message list for usage outside of main thread /// Returns an array copy of the message list for usage outside of main thread
/// </summary> /// </summary>
+10 -1
View File
@@ -120,7 +120,16 @@ public class FontManager
e => e.OnPreBuild( e => e.OnPreBuild(
tk => tk =>
{ {
var config = new SafeFontConfig {SizePt = Plugin.Config.GlobalFontV2.SizePt, GlyphRanges = Ranges}; // v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
// wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
// Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
// Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
// UseHellionFont=true wirkungslos, was 4K-User mit größerer
// Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
var basePt = Plugin.Config.UseHellionFont
? Plugin.Config.FontSizeV2
: Plugin.Config.GlobalFontV2.SizePt;
var config = new SafeFontConfig {SizePt = basePt, GlyphRanges = Ranges};
config.MergeFont = Plugin.Config.UseHellionFont config.MergeFont = Plugin.Config.UseHellionFont
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2") ? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global"); : AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
+1 -1
View File
@@ -4,7 +4,7 @@
0.1.0 is our bootstrap release; the underlying Chat 2 base is 0.1.0 is our bootstrap release; the underlying Chat 2 base is
called out in the yaml changelog so users can see what it called out in the yaml changelog so users can see what it
derives from. --> derives from. -->
<Version>1.1.0</Version> <Version>1.2.0</Version>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Honor packages.lock.json on restore so floating version ranges <!-- Honor packages.lock.json on restore so floating version ranges
+85
View File
@@ -55,6 +55,91 @@ tags:
- Replacement - Replacement
- Privacy - Privacy
changelog: |- changelog: |-
**Hellion Chat 1.2.0 — Layout Refresh**
Second cycle of the UI modernisation. Tab layouts modernised in
both modes, a new bottom status bar surfaces five live signals,
and messages render as card rows by default.
Tab layouts:
- Sidebar is now icon-only at fixed 44 px width. Tab name shows
as tooltip on hover. Active tab marked with a vertical accent
pill on the left window edge.
- Top tabs get an accent underline pill on the active tab
instead of a background fill.
- Per-tab custom icons via Settings → Tabs (15-glyph
FontAwesome picker). Default mapping covers General/System/
FreeCompany/Group/Linkshell/Tells/Auto-Tells.
Bottom status bar (22 px):
- Active channel with color dot
- Privacy-First badge
- Tab and message counters
- Auto-tell counter (hidden when zero)
- Plugin version (right-aligned, muted)
Updates 1×/second, cached.
Message rendering:
- Card rows are the new default. Sender header in channel color,
body on its own line, subtle border between cards.
- Compact-Density toggle in Appearance switches back to the
classic single-line `[HH:mm] Sender: Text` layout.
Migration:
- v14 → v15: legacy Configuration fields HellionThemeEnabled
and HellionThemeWindowOpacity removed. All other settings
preserved. Users who skip versions and migrate v13 → v15
directly will receive the default WindowOpacity (0.85);
re-adjust in Settings → Themes if needed.
Auto-Tell tabs:
- Each tell partner gets a hashed icon from a 7-glyph tell
pool (envelope/star/heart/bell/bookmark/flag/fire) plus a
hashed color from a 12-color palette. 84 distinct icon+
color combinations make parallel tells visually
distinguishable at a glance.
Unread indicator:
- Sidebar tabs with unread messages get a pulsing red dot in
the top-right corner. Subtle 2-second sine-wave pulse for
peripheral visibility without distraction. Respects
Configuration.ReduceMotion (UI toggle lands in v1.3.0).
Bug fixes from in-game testing:
- Settings save no longer wipes chat history by default. The
heavy ClearAllTabs + FilterAllTabsAsync refilter cycle now
only runs when a filter-relevant setting actually changed
(Privacy filter, persisted channels, per-tab channel
selection). Cosmetic changes — theme, tab icons, layout
flags — keep the in-session chat intact. Combined with an
Identifier-based MessageList restore in Configuration.
UpdateFrom and a TempTab skip in ClearAllTabs/FilterAllTabs,
persistent tabs and Auto-Tell tabs both survive the save.
- Sidebar buttons now align vertically with the first message
row (top padding mirrors the chat header toolbar height).
- Sidebar child window no longer paints the top padding with
its frame background.
- Status bar version slot ("vX.Y.Z · Hellion") no longer
clips its rightmost character.
- Top-tab icon prefix attempt was reverted: Dalamud's default
font atlas does not include FontAwesome codepoints, so
mixed-font in a single tab label renders as tofu. Underline
pill alone is the v1.2.0 visual treatment for top tabs.
Polish (lerps, theme crossfade, header quick-picker) follows
in v1.3.0. v1.2.0 is intentionally hard-switch — clean layout
first, motion next.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.1.0 — Theme Foundation** **Hellion Chat 1.1.0 — Theme Foundation**
First major UI cycle after the standalone v1.0.0 cut. Theme engine, First major UI cycle after the standalone v1.0.0 cut. Theme engine,
+12 -2
View File
@@ -151,7 +151,13 @@ internal class MessageManager : IAsyncDisposable
internal void ClearAllTabs() internal void ClearAllTabs()
{ {
foreach (var tab in Plugin.Config.Tabs) // Hellion Chat — TempTabs haben keine DB-Persistenz (session-only,
// direkt vom AutoTellTabsService befüllt). Ein Clear+Refilter würde
// sie leer hinterlassen weil FilterAllTabs nichts aus der DB
// findet — Tells sind oft durch Privacy-Filter blockiert oder
// schlicht session-flüchtig. TempTabs vom Clear-Pfad ausschließen
// damit Settings-Save den Tell-Verlauf nicht zerstört.
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
tab.Clear(); tab.Clear();
} }
@@ -165,7 +171,11 @@ internal class MessageManager : IAsyncDisposable
// We store the pending messages to be added to the chat log in a // We store the pending messages to be added to the chat log in a
// temporary list, and apply them all at once after filtering. // temporary list, and apply them all at once after filtering.
var pendingTabs = Plugin.Config.Tabs.Select(tab => (tab, new List<Message>())).ToList(); // TempTabs werden ausgeschlossen — sie bleiben live-state aus dem
// AutoTellTabsService, ein DB-Refilter würde sie nur partial
// wiederherstellen falls Tells in DB liegen, oder leer lassen wenn
// Privacy-Filter sie blockiert hat.
var pendingTabs = Plugin.Config.Tabs.Where(t => !t.IsTempTab).Select(tab => (tab, new List<Message>())).ToList();
foreach (var message in messages) foreach (var message in messages)
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message))) foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
pendingMessages.Add(message); pendingMessages.Add(message);
+19 -3
View File
@@ -64,6 +64,7 @@ public sealed class Plugin : IDalamudPlugin
internal TypingIpc TypingIpc { get; } internal TypingIpc TypingIpc { get; }
internal FontManager FontManager { get; } internal FontManager FontManager { get; }
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
internal Ui.StatusBar StatusBar { get; private set; } = null!;
internal int DeferredSaveFrames = -1; internal int DeferredSaveFrames = -1;
@@ -246,9 +247,8 @@ public sealed class Plugin : IDalamudPlugin
if (Config.Version < 14) if (Config.Version < 14)
{ {
Config.Theme = "hellion-arctic"; Config.Theme = "hellion-arctic";
#pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0 // v1.2.0: alter Opacity-Wert wird nicht mehr migriert (Field entfernt).
Config.WindowOpacity = Config.HellionThemeWindowOpacity; // User die direkt v13 → v15 springen bekommen den Default 0.85.
#pragma warning restore CS0612, CS0618
Config.ReduceMotion = false; Config.ReduceMotion = false;
Config.UseCompactDensity = false; Config.UseCompactDensity = false;
Config.ShowThemeQuickPicker = false; Config.ShowThemeQuickPicker = false;
@@ -259,6 +259,20 @@ public sealed class Plugin : IDalamudPlugin
"pick chat2-classic in Settings → Themes for the upstream look"); "pick chat2-classic in Settings → Themes for the upstream look");
} }
if (Config.Version < 15)
{
// v1.2.0 — keine Datenmigration nötig. Removal der deprecated
// Theme-Felder ist reine Schema-Bereinigung (System.Text.Json
// ignoriert unbekannte Felder im JSON, daher kein Crash bei
// Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity
// serialisiert haben — die Werte verfallen einfach).
Config.Version = 15;
SaveConfig();
Log.Information(
"Migrated config v14 → v15: legacy theme fields removed " +
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
}
// Hellion v1.0.0 default tab layout. Five thematically separated // Hellion v1.0.0 default tab layout. Five thematically separated
// tabs: General catches the immediate-surroundings public chat // tabs: General catches the immediate-surroundings public chat
// (Say/Yell/Shout) only; System absorbs the rest of the technical // (Say/Yell/Shout) only; System absorbs the rest of the technical
@@ -296,6 +310,8 @@ public sealed class Plugin : IDalamudPlugin
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
ThemeRegistry.Switch(Config.Theme); ThemeRegistry.Switch(Config.Theme);
StatusBar = new Ui.StatusBar();
MessageManager = new MessageManager(this); // Does it require UI? MessageManager = new MessageManager(this); // Does it require UI?
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
+13
View File
@@ -276,6 +276,11 @@ internal class HellionStrings
internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell)); internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell));
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint)); internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
// Hellion Chat — v1.2.0 per-tab icon override
internal static string Tabs_Icon_Label => Get(nameof(Tabs_Icon_Label));
internal static string Tabs_Icon_HelpMarker => Get(nameof(Tabs_Icon_HelpMarker));
internal static string Tabs_Icon_DefaultOption => Get(nameof(Tabs_Icon_DefaultOption));
// Hellion Chat — v0.6.0 chat colour presets (display labels) // Hellion Chat — v0.6.0 chat colour presets (display labels)
internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default)); internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default));
internal static string ChatColourPresets_HighContrast => Get(nameof(ChatColourPresets_HighContrast)); internal static string ChatColourPresets_HighContrast => Get(nameof(ChatColourPresets_HighContrast));
@@ -310,4 +315,12 @@ internal class HellionStrings
internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle)); internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle));
internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody)); internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody));
internal static string ChatTwoConflictAction => Get(nameof(ChatTwoConflictAction)); 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));
// Hellion Chat — v1.2.0 Appearance / Compact-Density toggle
internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name));
internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description));
} }
@@ -561,6 +561,17 @@
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve"> <data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value> <value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value>
</data> </data>
<!-- Hellion Chat — v1.2.0 per-tab icon override -->
<data name="Tabs_Icon_Label" xml:space="preserve">
<value>Tab-Icon</value>
</data>
<data name="Tabs_Icon_HelpMarker" xml:space="preserve">
<value>FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ.</value>
</data>
<data name="Tabs_Icon_DefaultOption" xml:space="preserve">
<value>(Default-Mapping)</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve"> <data name="ChatColourPresets_Default" xml:space="preserve">
<value>Klassik (Chat 2 Default)</value> <value>Klassik (Chat 2 Default)</value>
</data> </data>
@@ -705,4 +716,16 @@
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve"> <data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
<value>Behalten</value> <value>Behalten</value>
</data> </data>
<data name="StatusBar_Privacy_Enabled" xml:space="preserve">
<value>Privacy-First</value>
</data>
<data name="StatusBar_Privacy_Open" xml:space="preserve">
<value>Offen</value>
</data>
<data name="Appearance_UseCompactDensity_Name" xml:space="preserve">
<value>Kompakte Dichte</value>
</data>
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
</data>
</root> </root>
+23
View File
@@ -561,6 +561,17 @@
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve"> <data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value> <value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value>
</data> </data>
<!-- Hellion Chat — v1.2.0 per-tab icon override -->
<data name="Tabs_Icon_Label" xml:space="preserve">
<value>Tab-Icon</value>
</data>
<data name="Tabs_Icon_HelpMarker" xml:space="preserve">
<value>FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ.</value>
</data>
<data name="Tabs_Icon_DefaultOption" xml:space="preserve">
<value>(Default-Mapping)</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve"> <data name="ChatColourPresets_Default" xml:space="preserve">
<value>Klassik (Chat 2 Default)</value> <value>Klassik (Chat 2 Default)</value>
</data> </data>
@@ -705,4 +716,16 @@
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve"> <data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
<value>Keep current</value> <value>Keep current</value>
</data> </data>
<data name="StatusBar_Privacy_Enabled" xml:space="preserve">
<value>Privacy-First</value>
</data>
<data name="StatusBar_Privacy_Open" xml:space="preserve">
<value>Open</value>
</data>
<data name="Appearance_UseCompactDensity_Name" xml:space="preserve">
<value>Compact Density</value>
</data>
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
</data>
</root> </root>
+107
View File
@@ -0,0 +1,107 @@
namespace HellionChat.Ui;
/// <summary>
/// Hash-Color-Tinting für Auto-Tell-Tabs in der Sidebar (v1.2.0).
/// Differenziert Tells visuell ohne dass User pro Tab manuell ein
/// Custom-Icon setzen muss. Gleicher Tell-Partner (Name+World) liefert
/// konsistent dieselbe Farbe über Sessions hinweg.
///
/// Kuratierte 12-Farb-Palette aus dem Hellion-Theme-Pool: alle saturiert
/// mid-bright, lesbar gegen Dark-Theme-Backgrounds. Bei realistischen
/// 1-5 parallelen Tells ist Kollisions-Wahrscheinlichkeit gering.
///
/// Reine String-Logik (kein Dalamud-Dep) — testbar im HellionChat.Tests-
/// Projekt das ohne Dalamud-Reference baut.
/// </summary>
internal static class AutoTellTabTint
{
/// <summary>
/// Fallback bei ungültigem Input (leerer Name, World=0). Standard-
/// Text-Color (weiß) — passt mit existierendem TextPrimary-Default
/// zusammen, sodass die Sidebar visuell konsistent bleibt.
/// </summary>
public const uint Fallback = 0xFFFFFFFFu;
/// <summary>
/// 12 saturierte mid-bright Farben aus den 5 Built-In-Themes
/// (Hellion-Arctic, Chat2-Klassik, Event-Horizon, Moonlit-Bloom,
/// Mint-Grove). Reihenfolge ist deterministisch — Hash-Index wählt
/// Farbe per Modulo. RGBA-Format (passt zu ColourUtil.RgbaToAbgr-
/// Konvention im restlichen Code).
/// </summary>
public static readonly IReadOnlyList<uint> Palette = new uint[]
{
0x00BED2FFu, // Arctic Cyan
0xF97316FFu, // Ember Orange
0xB585FFFFu, // Light Cosmic Purple
0xE374E8FFu, // Bloom Magenta
0x5DD39EFFu, // Mint Green
0xF0AD4EFFu, // Warning Yellow
0xE85C6AFFu, // Coral
0x5CB85CFFu, // Status Green
0x6278FFFFu, // Bloom Blue
0xC9982EFFu, // Warm Gold
0x9CCB7CFFu, // Soft Sage
0xE85D04FFu, // Deep Ember
};
/// <summary>
/// Liefert eine konsistente Tint-Color für einen Tell-Partner.
/// Hash basiert auf "Name@World" — Cross-World-Namen kollidieren
/// nur bei Hash-Bucket-Kollision, nicht durch Identitäts-Annahme.
/// </summary>
public static uint For(string name, uint world)
{
if (string.IsNullOrEmpty(name) || world == 0)
return Fallback;
// GetHashCode kann negativ sein; Bitmaske auf positive Range
// damit Modulo-Division immer einen validen Index liefert.
var key = $"{name}@{world}";
var hash = (uint)(key.GetHashCode() & 0x7FFFFFFF);
return Palette[(int)(hash % Palette.Count)];
}
/// <summary>
/// Tell-spezifischer Icon-Pool. 7 visuell distinkte FontAwesome-Glyphen
/// die im Tell-Kontext sinnvoll wirken (envelope = Tell-Default, star/
/// heart/bell = personalisiert, bookmark/flag/fire = markiert/wichtig).
/// Bewusst kein cog/comment/users — die wären für System-/Group-Tabs
/// reserviert und würden im Tell-Bereich verwirrend wirken.
/// </summary>
public static readonly IReadOnlyList<string> IconPool = new[]
{
"envelope",
"star",
"heart",
"bell",
"bookmark",
"flag",
"fire",
};
/// <summary>
/// Fallback-Icon bei ungültigem Input. "envelope" passt semantisch zum
/// Tell-Kontext besser als das alte hardcoded "clock".
/// </summary>
public const string IconFallback = "envelope";
/// <summary>
/// Liefert ein konsistentes Icon-Glyph für einen Tell-Partner.
/// Nutzt einen anderen Hash-Bias als For() (Color), damit Icon und
/// Color unabhängig variieren — gibt 7 × 12 = 84 distinct Combinations.
/// </summary>
public static string IconFor(string name, uint world)
{
if (string.IsNullOrEmpty(name) || world == 0)
return IconFallback;
// Anderer Hash-Bias als For() (verschiedene Modulo-Basis): wir
// nutzen "world@name" statt "name@world" damit Icon und Color
// nicht synchron variieren. Ohne Bias-Trennung würden alle Tells
// mit derselben Color auch dasselbe Icon haben.
var key = $"{world}@{name}";
var hash = (uint)(key.GetHashCode() & 0x7FFFFFFF);
return IconPool[(int)(hash % IconPool.Count)];
}
}
+180 -26
View File
@@ -375,6 +375,9 @@ public sealed class ChatLogWindow : Window
// weil der Cursor schon weiter unten steht — kein eigener Abzug. // weil der Cursor schon weiter unten steht — kein eigener Abzug.
height -= ImGui.GetFrameHeightWithSpacing(); height -= ImGui.GetFrameHeightWithSpacing();
// v1.2.0 — Status-Bar am Window-Boden reserviert 22 px + 2 px Spacing.
height -= StatusBar.Height + 2;
return height; return height;
} }
@@ -790,15 +793,19 @@ public sealed class ChatLogWindow : Window
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
LastActivityTime = FrameTime; LastActivityTime = FrameTime;
if (!showNovice) if (showNovice)
return; {
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf)) if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf))
GameFunctions.GameFunctions.ClickNoviceNetworkButton(); GameFunctions.GameFunctions.ClickNoviceNetworkButton();
} }
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
Plugin.StatusBar.Draw(Plugin);
}
internal Dictionary<string, InputChannel> GetValidChannels() internal Dictionary<string, InputChannel> GetValidChannels()
{ {
var channels = new Dictionary<string, InputChannel>(); var channels = new Dictionary<string, InputChannel>();
@@ -1316,6 +1323,51 @@ public sealed class ChatLogWindow : Window
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var lineWidth = ImGui.GetContentRegionAvail().X; var lineWidth = ImGui.GetContentRegionAvail().X;
// v1.2.0 — Card-Rows als Default, Compact-Density als Opt-Out.
// Card-Mode: Sender-Header in Channel-Color auf eigener Zeile,
// dann Body, dann subtile Border-Bottom als Card-Trenner.
// Compact-Mode: bisheriges Verhalten — Sender + Space + Content
// auf einer Zeile via SameLine.
var useCard = !Plugin.Config.UseCompactDensity;
if (useCard)
{
if (message.Sender.Count > 0)
{
var theme = Plugin.ThemeRegistry.Active;
var senderColor = Plugin.Functions.Chat.GetChannelColor(message.Code.Type)
?? theme.Colors.TextPrimary;
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(senderColor)))
{
DrawChunks(message.Sender, true, handler, lineWidth);
}
// KEIN SameLine — Body landet auf eigener Zeile.
}
// We need to draw something otherwise the item visibility check below won't work.
if (message.Content.Count == 0)
DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth);
else
DrawChunks(message.Content, true, handler, lineWidth);
// Subtile Border-Bottom als Card-Trenner. Border-Farbe mit
// reduzierter Alpha (RGBA → 0x33) für dezente Trennung.
{
var theme = Plugin.ThemeRegistry.Active;
var rowEndY = ImGui.GetCursorScreenPos().Y;
var winLeft = ImGui.GetWindowPos().X;
var winRight = winLeft + ImGui.GetWindowSize().X;
var borderRgba = (theme.Colors.Border & 0xFFFFFF00u) | 0x33u;
ImGui.GetWindowDrawList().AddLine(
new Vector2(winLeft + 4, rowEndY - 1),
new Vector2(winRight - 4, rowEndY - 1),
ColourUtil.RgbaToAbgr(borderRgba),
1f);
ImGui.Dummy(new Vector2(0, 2));
}
}
else
{
if (message.Sender.Count > 0) if (message.Sender.Count > 0)
{ {
DrawChunks(message.Sender, true, handler, lineWidth); DrawChunks(message.Sender, true, handler, lineWidth);
@@ -1327,6 +1379,7 @@ public sealed class ChatLogWindow : Window
DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth);
else else
DrawChunks(message.Content, true, handler, lineWidth); DrawChunks(message.Content, true, handler, lineWidth);
}
message.IsVisible[tab.Identifier] = ImGui.IsItemVisible(); message.IsVisible[tab.Identifier] = ImGui.IsItemVisible();
} }
@@ -1366,6 +1419,20 @@ public sealed class ChatLogWindow : Window
if (!tabItem.Success) if (!tabItem.Success)
continue; continue;
// v1.2.0 — Active-Tab-Underline-Pill (2 px Akzent statt Background-Fill).
// Bewusst direkt nach TabItem-Setup; GetItemRectMin/Max referenziert noch
// das Tab. ImGui hat keine native Underline-API, daher direkter DrawList-Pass.
{
var theme = Plugin.ThemeRegistry.Active;
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
const float pillHeight = 2f;
ImGui.GetWindowDrawList().AddRectFilled(
new Vector2(min.X, max.Y - pillHeight),
new Vector2(max.X, max.Y),
ColourUtil.RgbaToAbgr(theme.Colors.Accent));
}
var hasTabSwitched = Plugin.LastTab != tabI; var hasTabSwitched = Plugin.LastTab != tabI;
Plugin.LastTab = tabI; Plugin.LastTab = tabI;
@@ -1383,21 +1450,36 @@ public sealed class ChatLogWindow : Window
private void DrawTabSidebar() private void DrawTabSidebar()
{ {
var currentTab = -1; var currentTab = -1;
using var tabTable = ImRaii.Table("tabs-table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.Resizable); // v1.2.0 — Sidebar fix 44 px, kein Resize. Mehr Platz fürs Chat-Log.
using var tabTable = ImRaii.Table("tabs-table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit);
if (!tabTable.Success) if (!tabTable.Success)
return; return;
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthStretch, 1); ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f);
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 4); ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var hasTabSwitched = false; var hasTabSwitched = false;
var childHeight = GetRemainingHeightForMessageLog(); var childHeight = GetRemainingHeightForMessageLog();
// v1.2.0 — Sidebar-Child ohne Theme-ChildBg, sonst füllt das
// bläuliche Frame-Rect auch den oberen HeaderToolbar-Padding-Bereich
// aus (sieht aus wie ein angeschnittener Block oberhalb der Buttons).
// Vertikale Trennung zur Message-Spalte bleibt durch BordersInnerV
// der Tab-Table erhalten.
using (ImRaii.PushColor(ImGuiCol.ChildBg, 0u))
using (var child = ImRaii.Child("##chat2-tab-sidebar", new Vector2(-1, childHeight))) using (var child = ImRaii.Child("##chat2-tab-sidebar", new Vector2(-1, childHeight)))
{ {
if (child) if (child)
{ {
// v1.2.0 — Top-Padding spiegelt die HeaderToolbar-Höhe der
// rechten Spalte (DrawChatHeaderToolbar wird dort als erstes
// gerendert, eine Frame-Zeile + ItemSpacing). Ohne diesen
// Padding würden die Sidebar-Buttons oben am Window-Top
// kleben, während die Messages erst unter der Toolbar
// beginnen — vertikales Mismatch.
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
var previousTab = Plugin.CurrentTab; var previousTab = Plugin.CurrentTab;
// Hellion Chat — auto-tell-tabs section divider rendered // Hellion Chat — auto-tell-tabs section divider rendered
// exactly once before the first temp tab, with a live unit // exactly once before the first temp tab, with a live unit
@@ -1422,7 +1504,6 @@ public sealed class ChatLogWindow : Window
} }
var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})";
var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}";
var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI; var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI;
var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle; var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
@@ -1457,34 +1538,107 @@ public sealed class ChatLogWindow : Window
ImGui.SameLine(); ImGui.SameLine();
} }
bool clicked; // v1.2.0 — Icon-only Sidebar mit Tooltip beim Hover.
if (showGreetedAffordance && tab.IsGreeted) // Active-Tab kriegt Akzent-Color am Icon, Greeted-Tabs
// werden auf TextDim gedimmt (löst den alten Header-
// Dim-Trick ab, da wir keine Selectable mehr nutzen).
var theme = Plugin.ThemeRegistry.Active;
var icon = TabIconMapping.Resolve(tab);
uint iconColor;
if (isCurrentTab)
{ {
// Dim the tab name once the user marked the partner iconColor = theme.Colors.Accent;
// as greeted, so a glance at the sidebar tells them
// who still needs attention. Selectable has no idle
// background slot in ImGui, so the dim only applies
// to the selected and hovered states — the text dim
// alone signals greeted in the idle state.
var headerBase = ImGui.GetColorU32(ImGuiCol.Header);
var hoverBase = ImGui.GetColorU32(ImGuiCol.HeaderHovered);
var dimHeader = (headerBase & 0xFF000000u) | ((headerBase & 0x00FEFEFEu) >> 1);
var dimHover = (hoverBase & 0xFF000000u) | ((hoverBase & 0x00FEFEFEu) >> 1);
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)))
using (ImRaii.PushColor(ImGuiCol.Header, dimHeader))
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, dimHover))
{
clicked = ImGui.Selectable(selectableLabel, isCurrentTab);
} }
else if (showGreetedAffordance && tab.IsGreeted)
{
iconColor = theme.Colors.TextDim;
}
else if (tab.IsTempTab && tab.TellTarget != null && tab.TellTarget.IsSet())
{
// v1.2.0 — Hash-Color-Tint differenziert parallele Auto-Tell-Tabs
// visuell ohne dass User pro Tab manuell ein Custom-Icon setzen muss.
iconColor = AutoTellTabTint.For(tab.TellTarget.Name, tab.TellTarget.World);
} }
else else
{ {
clicked = ImGui.Selectable(selectableLabel, isCurrentTab); iconColor = theme.Colors.TextPrimary;
}
bool clicked;
using (ImRaii.PushColor(ImGuiCol.Button, 0u))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.RgbaToAbgr(theme.Colors.SurfaceHover)))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.RgbaToAbgr(theme.Colors.Surface)))
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
using (Plugin.FontManager.FontAwesome.Push())
{
clicked = ImGui.Button($"{icon.ToIconString()}##sidebar-tab-{tabI}", new Vector2(36f, ImGui.GetFrameHeight()));
}
if (isCurrentTab)
{
// v1.2.0 — Vertikale Akzent-Pill an der linken Window-Kante.
// 3 px breit, halbe Tab-Höhe, vertikal zentriert. ImGui hat keine
// native Pill-API, daher direkter DrawList-Pass.
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
const float pillWidth = 3f;
var pillHeight = (max.Y - min.Y) * 0.5f;
var pillCenterY = (min.Y + max.Y) * 0.5f;
ImGui.GetWindowDrawList().AddRectFilled(
new Vector2(min.X, pillCenterY - pillHeight * 0.5f),
new Vector2(min.X + pillWidth, pillCenterY + pillHeight * 0.5f),
ColourUtil.RgbaToAbgr(theme.Colors.Accent),
1.5f); // leichter Rounding
}
// v1.2.0 — Unread-Dot oben rechts am Icon. Sichtbar ohne Hover, damit
// User Tabs mit ungelesenen Messages sofort erkennt. Aktive Tabs haben
// per Konvention Unread = 0 (LastTab-Branch in ChatLogWindow), daher
// kollidiert der Dot nicht mit der Active-Pill.
if (!isCurrentTab && tab.UnreadMode != UnreadMode.None && tab.Unread > 0)
{
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
const float dotRadius = 4f;
const float dotPadding = 3f;
var dotCenter = new Vector2(
max.X - dotRadius - dotPadding,
min.Y + dotRadius + dotPadding);
// v1.2.0 — Sanfter Pulse-Effekt: Alpha schwankt zwischen 60% und
// 100% mit ~2-Sekunden-Cycle (subtil, nicht hektisch).
// Plugin.Config.ReduceMotion (Field seit v1.1.0) skipt den Pulse
// und rendert statisch — Default ist Animation an.
var dotColor = theme.Colors.StatusDanger;
if (!Plugin.Config.ReduceMotion)
{
// Sin-basierter 2s-Cycle: -1..1 → 0..1 → 0.6..1.0 Alpha-Skala.
var phase = (float)((Math.Sin(Environment.TickCount64 / 1000.0 * Math.PI) + 1.0) * 0.5);
var alphaScale = 0.6f + 0.4f * phase;
var origAlpha = dotColor & 0xFFu;
var pulsedAlpha = (uint)(origAlpha * alphaScale);
dotColor = (dotColor & 0xFFFFFF00u) | pulsedAlpha;
}
ImGui.GetWindowDrawList().AddCircleFilled(
dotCenter,
dotRadius,
ColourUtil.RgbaToAbgr(dotColor),
12);
}
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted($"{tab.Name}{unread}");
} }
DrawTabContextMenu(tab, tabI); DrawTabContextMenu(tab, tabI);
if (clicked)
Plugin.WantedTab = tabI;
if (!clicked && Plugin.WantedTab != tabI) if (!clicked && Plugin.WantedTab != tabI)
continue; continue;
+65
View File
@@ -210,14 +210,24 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001 var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001; || Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled; var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
// v1.2.0 — Refilter only if a filter-relevant setting actually
// changed. The Clear+Refilter cycle reloads messages from the DB,
// which silently wipes any in-session message that wasn't
// persisted (Privacy-First config blocks most channels from DB).
// Cosmetic changes (theme, tab icons, layout flags) trigger no
// refilter — chat history stays intact.
var filtersChanged = HasFilterRelevantChanges();
Plugin.Config.UpdateFrom(Mutable, true); Plugin.Config.UpdateFrom(Mutable, true);
// save after 60 frames have passed, which should hopefully not // save after 60 frames have passed, which should hopefully not
// commit any changes that cause a crash // commit any changes that cause a crash
Plugin.DeferredSaveFrames = 60; Plugin.DeferredSaveFrames = 60;
if (filtersChanged)
{
Plugin.MessageManager.ClearAllTabs(); Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync(); Plugin.MessageManager.FilterAllTabsAsync();
}
if (fontChanged || fontSizeChanged || italicStateChanged) if (fontChanged || fontSizeChanged || italicStateChanged)
Plugin.FontManager.BuildFonts(); Plugin.FontManager.BuildFonts();
@@ -233,4 +243,59 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
Initialise(); Initialise();
} }
/// <summary>
/// v1.2.0 — Detects whether any setting that influences message
/// filtering changed between Plugin.Config and the Mutable working
/// copy. Used to gate the heavy ClearAllTabs+FilterAllTabsAsync cycle
/// in Save: cosmetic changes (theme, tab icons, layout flags) do not
/// touch the chat log, only filter-relevant changes do. Without this
/// gate, every settings save wipes the chat history of any channel
/// the Privacy filter blocks from being persisted to the DB —
/// reported by Flo from in-game testing 2026-05-05/06.
/// </summary>
private bool HasFilterRelevantChanges()
{
// Top-level privacy controls.
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled) return true;
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels) return true;
if (!Mutable.PrivacyPersistChannels.SetEquals(Plugin.Config.PrivacyPersistChannels)) return true;
// FilterIncludePreviousSessions changes the GetMostRecentMessages
// window in MessageManager.FilterAllTabs and is therefore filter-
// relevant even though it lives outside the Privacy block.
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions) return true;
// Per-tab channel selection. Compare persistent tabs only —
// TempTabs are session-only and never refiltered anyway.
var origPersistent = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
var newPersistent = Mutable.Tabs.Where(t => !t.IsTempTab).ToList();
if (origPersistent.Count != newPersistent.Count) return true; // add or delete
for (var i = 0; i < origPersistent.Count; i++)
{
var orig = origPersistent[i];
var neu = newPersistent[i];
// Identifier mismatch at the same index means reorder or
// a slot got swapped — treat as filter-relevant so the new
// channel-selection layout actually applies.
if (orig.Identifier != neu.Identifier) return true;
if (orig.ExtraChatAll != neu.ExtraChatAll) return true;
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels)) return true;
// SelectedChannels is a Dictionary<ChatType, (ChatSource, ChatSource)>
// — value-tuple equality already does the right thing per-pair.
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count) return true;
foreach (var pair in orig.SelectedChannels)
{
if (!neu.SelectedChannels.TryGetValue(pair.Key, out var nv)) return true;
if (!pair.Value.Equals(nv)) return true;
}
}
return false;
}
} }
+26 -36
View File
@@ -45,32 +45,11 @@ internal sealed class Appearance : ISettingsTab
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{ {
// v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten // v1.2.0 — Legacy HellionThemeEnabled/HellionThemeWindowOpacity-Bindings
// Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten, // entfernt. Theme-Auswahl + globale Window-Opacity leben jetzt in
// damit die Settings-Seite kompiliert; sie schreiben in die mit // Settings → Themes (eingeführt mit v1.1.0). Hier verbleibt nur der
// [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net // klassische OverrideStyle-Toggle plus der Bestand-WindowAlpha-Slider
// bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen // für das Chat-Log-Fenster.
// gezielt für diesen Übergangs-Block.
#pragma warning disable CS0612, CS0618
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
// Clamp 0.51.0 stays consistent with Privacy.cs which already
// shipped this slider; lower values would let chat windows
// disappear behind game UI.
using (ImRaii.Disabled(!Mutable.HellionThemeEnabled))
{
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
var opacity = Mutable.HellionThemeWindowOpacity;
if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f"))
{
Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f);
}
ImGuiUtil.HelpMarker(HellionStrings.Theme_WindowOpacity_Help);
}
ImGui.Spacing();
ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle); ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle);
ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc); ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc);
@@ -79,17 +58,8 @@ internal sealed class Appearance : ISettingsTab
DrawStyleCombo(); DrawStyleCombo();
} }
// The Bestand-Slider WindowAlpha targets the chat log window's
// background only. The Hellion theme opacity above already covers
// every plugin window globally, so the two sliders fight each
// other when the theme is active. Disable the legacy slider in
// that case to make Hellion theme the single source of truth.
using (ImRaii.Disabled(Mutable.HellionThemeEnabled))
{
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
} }
#pragma warning restore CS0612, CS0618
}
} }
private void DrawStyleCombo() private void DrawStyleCombo()
@@ -139,7 +109,22 @@ internal sealed class Appearance : ISettingsTab
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description); ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
ImGui.Spacing(); ImGui.Spacing();
using var fontDisabled = ImRaii.Disabled(Mutable.UseHellionFont); // v1.2.0 — Schriftgröße muss auch bei aktiver Hellion-Schrift
// editierbar sein (Exo 2 ist Variable-Font, FontSizeV2 wird in
// FontManager als SizePt angewendet). Disabled-Wrap nur noch
// um den Bestand-Custom-Font-Stack (FontsEnabled-Toggle und
// die Font-Chooser) — der ist weiter exclusive zu HellionFont.
if (Mutable.UseHellionFont)
{
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
ImGui.Spacing();
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
ImGuiUtil.HelpMarker(Language.Options_SymbolsFontSize_Description);
ImGui.Spacing();
return;
}
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled); ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
ImGui.Spacing(); ImGui.Spacing();
@@ -356,6 +341,11 @@ internal sealed class Appearance : ISettingsTab
ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty); ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty);
ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description); ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description);
// v1.2.0 — Card-Rows als Default. Compact-Density schaltet auf den
// klassischen Single-Line-Mode `[HH:mm] Sender: Text` zurück.
ImGui.Checkbox(HellionStrings.Appearance_UseCompactDensity_Name, ref Mutable.UseCompactDensity);
ImGuiUtil.HelpMarker(HellionStrings.Appearance_UseCompactDensity_Description);
ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps); ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps);
ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description); ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description);
} }
+34
View File
@@ -91,6 +91,40 @@ internal sealed class Tabs : ISettingsTab
} }
ImGui.InputText(Language.Options_Tabs_Name, ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue); ImGui.InputText(Language.Options_Tabs_Name, ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue);
// v1.2.0 — Per-Tab Icon-Override. Default-Mapping greift falls nichts gesetzt.
ImGui.TextUnformatted(HellionStrings.Tabs_Icon_Label);
ImGui.SameLine();
ImGuiUtil.HelpMarker(HellionStrings.Tabs_Icon_HelpMarker);
var iconCurrent = string.IsNullOrEmpty(tab.Icon) ? "" : tab.Icon;
var iconPreview = iconCurrent.Length == 0
? HellionStrings.Tabs_Icon_DefaultOption
: iconCurrent;
using (var combo = ImRaii.Combo($"##icon-{i}", iconPreview))
{
if (combo.Success)
{
// Erste Option: Default (löscht Icon, lässt Mapping greifen).
if (ImGui.Selectable(HellionStrings.Tabs_Icon_DefaultOption, iconCurrent.Length == 0))
{
tab.Icon = null;
}
ImGui.Separator();
// Pool-Optionen aus TabIconGlyphResolver.PickerOptions (Single-Source-of-Truth).
foreach (var option in TabIconGlyphResolver.PickerOptions)
{
var isSelected = string.Equals(iconCurrent, option, StringComparison.OrdinalIgnoreCase);
if (ImGui.Selectable(option, isSelected))
{
tab.Icon = option;
}
}
}
}
ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp); ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp);
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut); ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
if (tab.PopOut) if (tab.PopOut)
+169
View File
@@ -0,0 +1,169 @@
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;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Reine String-Logik — testbar ohne ImGui-Init.
/// </summary>
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}";
}
/// <summary>
/// Reine String-Logik — testbar ohne ImGui-Init.
/// 0 Tells → Leerstring (Slot wird ausgeblendet).
/// </summary>
public static string FormatTells(int count)
{
if (count <= 0) return string.Empty;
return $"{count} {(count == 1 ? "tell" : "tells")}";
}
/// <summary>
/// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren.
/// Nicht für Production-Render.
/// </summary>
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;
}
/// <summary>
/// Render-Pfad. Aufrufer pusht bereits den HellionStyle/Theme;
/// wir lesen nur die aktiven Theme-Farben und zeichnen.
/// </summary>
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
// 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 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("·");
}
}
+79
View File
@@ -0,0 +1,79 @@
namespace HellionChat.Ui;
/// <summary>
/// Reine String-Resolver-Logik ohne Dalamud-Dependency. Bewusst in
/// eigener Datei (Dependency-Boundary auf File-Level sichtbar), damit
/// Tests (HellionChat.Tests, Microsoft.NET.Sdk ohne Dalamud-Reference)
/// sie aufrufen können, ohne dass die JIT beim Methodenaufruf die
/// Dalamud-Assembly laden muss.
///
/// Wird im Settings-UI (T7) für die Glyph-Picker-Combobox und im
/// Render-Code indirekt über <see cref="TabIconMapping.Resolve(Tab)"/>
/// verwendet.
/// </summary>
internal static class TabIconGlyphResolver
{
/// <summary>
/// Picker-Options-Pool — Single Source of Truth für das Glyph-Set.
/// Reihenfolge ist die UI-Reihenfolge im Settings-Tab Icon-Combobox.
/// </summary>
public static readonly IReadOnlyList<string> PickerOptions =
["comment", "comments", "cog", "users", "user-friends", "link",
"envelope", "clock", "hashtag", "star", "heart", "bell",
"bookmark", "flag", "fire"];
/// <summary>
/// Glyph-Set, das überhaupt als Override akzeptiert wird. Aus
/// <see cref="PickerOptions"/> abgeleitet — KnownGlyphs nie
/// manuell pflegen.
/// </summary>
private static readonly HashSet<string> KnownGlyphs =
new(PickerOptions, StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Tab-Name → Default-Glyph-Name. Tab.Name wird per Lokalisierung
/// gesetzt; wir matchen daher gegen einen Pool aus DE/EN-Synonymen.
/// </summary>
private static readonly Dictionary<string, string> NameDefaults = new(StringComparer.OrdinalIgnoreCase)
{
["allgemein"] = "comment",
["general"] = "comment",
["system"] = "cog",
["free company"] = "users",
["fc"] = "users",
["gruppe"] = "user-friends",
["group"] = "user-friends",
["party"] = "user-friends",
["linkshell"] = "link",
["ls"] = "link",
["cwls"] = "link",
["tells"] = "envelope",
["tell"] = "envelope",
};
/// <summary>
/// Test-Surface: Glyph-Name-Resolver ohne Dalamud-Dependency.
/// Reihenfolge:
/// 1. Tab.Icon-Override (falls gesetzt und nicht nur Whitespace):
/// a) bekannter Glyph → diesen Glyph
/// b) unbekannter Glyph → harter Fallback "hashtag" (User hat
/// bewusst etwas gesetzt, also überstimmt das die Defaults)
/// 2. Auto-Tell-Tab → <paramref name="autoTellGlyph"/> falls
/// übergeben, sonst "clock".
/// 3. Tab-Name-Default (<see cref="NameDefaults"/>-Lookup)
/// 4. Fallback "hashtag"
/// </summary>
public static string ResolveGlyphName(Tab tab, string? autoTellGlyph = null)
{
if (!string.IsNullOrWhiteSpace(tab.Icon))
return KnownGlyphs.Contains(tab.Icon) ? tab.Icon : "hashtag";
if (tab.IsTempTab)
return autoTellGlyph ?? "clock";
if (tab.Name is { } name && NameDefaults.TryGetValue(name, out var byName))
return byName;
return "hashtag";
}
}
+72
View File
@@ -0,0 +1,72 @@
using Dalamud.Interface;
namespace HellionChat.Ui;
/// <summary>
/// Default-Icon-Mapping für Tabs. v1.2.0 Layout-Refresh nutzt das
/// in Top-Tabs (Icon-Prefix) und Sidebar (Icon-only mit Tooltip).
/// User können in Settings → Tabs per Tab.Icon-Override eigene
/// FontAwesome-Glyphen setzen.
///
/// Diese Klasse ist Dalamud-abhängig (FontAwesomeIcon-Enum). Die
/// reine String-Resolver-Logik liegt bewusst in
/// <see cref="TabIconGlyphResolver"/> (eigene Datei, ohne
/// Dalamud-Imports), damit Tests sie ohne Dalamud-Reference aufrufen
/// können.
/// </summary>
internal static class TabIconMapping
{
/// <summary>
/// FontAwesome-Glyph-Name → Icon-Enum-Lookup. Wird für die
/// Production-Resolve-API benötigt.
///
/// INVARIANTE: Jeder Key in <see cref="GlyphLookup"/> muss auch in
/// <see cref="TabIconGlyphResolver.PickerOptions"/> stehen. Wird
/// ein Glyph zu PickerOptions hinzugefügt, aber nicht hier, fällt
/// die Override-Auflösung still auf <see cref="FontAwesomeIcon.Hashtag"/>
/// zurück (degraded, kein Crash). Build-Time-Enforcement ist nicht
/// möglich, weil PickerOptions ohne Dalamud-Reference auskommt.
/// </summary>
private static readonly Dictionary<string, FontAwesomeIcon> GlyphLookup = new(StringComparer.OrdinalIgnoreCase)
{
["comment"] = FontAwesomeIcon.Comment,
["comments"] = FontAwesomeIcon.Comments,
["cog"] = FontAwesomeIcon.Cog,
["users"] = FontAwesomeIcon.Users,
["user-friends"] = FontAwesomeIcon.UserFriends,
["link"] = FontAwesomeIcon.Link,
["envelope"] = FontAwesomeIcon.Envelope,
["clock"] = FontAwesomeIcon.Clock,
["hashtag"] = FontAwesomeIcon.Hashtag,
["star"] = FontAwesomeIcon.Star,
["heart"] = FontAwesomeIcon.Heart,
["bell"] = FontAwesomeIcon.Bell,
["bookmark"] = FontAwesomeIcon.Bookmark,
["flag"] = FontAwesomeIcon.Flag,
["fire"] = FontAwesomeIcon.Fire,
};
/// <summary>
/// Production-Surface: liefert das Icon für einen Tab. Wrapper um
/// <see cref="TabIconGlyphResolver.ResolveGlyphName(Tab)"/> plus
/// Enum-Lookup. Wird von Render-Code (T3, T5) verwendet.
/// </summary>
public static FontAwesomeIcon Resolve(Tab tab)
{
// v1.2.0 — Auto-Tell-Tabs bekommen ein per-Partner gehashtes
// Icon aus dem Tell-Pool. Damit unterscheiden sich parallele
// Tells nicht nur über die Color (For), sondern auch über die
// Glyph-Form. Berechnung bleibt hier (Dalamud-bound), weil
// TellTarget Dalamud-Imports hat.
string? autoTellGlyph = null;
if (tab.IsTempTab && tab.TellTarget != null && tab.TellTarget.IsSet())
{
autoTellGlyph = AutoTellTabTint.IconFor(tab.TellTarget.Name, tab.TellTarget.World);
}
var glyph = TabIconGlyphResolver.ResolveGlyphName(tab, autoTellGlyph);
return GlyphLookup.TryGetValue(glyph, out var icon)
? icon
: FontAwesomeIcon.Hashtag;
}
}
+27
View File
@@ -12,6 +12,33 @@ und verlinkt für Details auf die Release-Pages.
--- ---
## v1.2.0 — Layout Refresh (2026-05-05)
### Added
- Sidebar tab modernization: icon-only at fixed 44 px, tooltip on hover, vertical accent pill for active tab
- Top tabs: accent underline pill replaces background fill on active tab
- Per-tab custom icons in Settings → Tabs (15-glyph FontAwesome picker)
- Bottom status bar (22 px): channel indicator, privacy badge, counters, tells, version — updates 1×/sec
- Card rows as default message render: sender header in channel color, subtle border between cards
- Compact-Density toggle in Appearance: switches back to single-line `[HH:mm] Sender: Text` layout
- Auto-Tell tabs: per-partner hashed icon (7-glyph pool: envelope/star/heart/bell/bookmark/flag/fire) plus hashed color (12-color palette) — 84 distinct icon+color combinations
- Unread indicator: pulsing red dot in the top-right corner of any sidebar tab icon with unread messages, 2-second sine-wave pulse, respects `Configuration.ReduceMotion`
### Changed
- Migration v14 → v15: deprecated Configuration fields `HellionThemeEnabled` and `HellionThemeWindowOpacity` removed
- Appearance settings cleaned: legacy theme-engine bindings replaced by Themes tab (introduced in v1.1.0)
### Fixed
- Settings save no longer wipes chat history by default — the heavy `ClearAllTabs + FilterAllTabsAsync` cycle now only runs when a filter-relevant setting actually changed (Privacy filter, persisted channels, per-tab channel selection). Cosmetic changes keep the in-session chat intact
- Identifier-based `MessageList` restore in `Configuration.UpdateFrom` plus TempTab skip in `ClearAllTabs`/`FilterAllTabs` ensure persistent tabs and Auto-Tell tabs both survive the save
- Sidebar buttons now align vertically with the first message row (top padding mirrors the chat header toolbar height)
- Sidebar child window no longer paints the top padding area with its frame background
- Status bar version slot (`vX.Y.Z · Hellion`) no longer clips its rightmost character
### Notes
- Polish phase (animations, theme crossfade, header quick-picker) follows in v1.3.0
- Top-Tab icon prefixes were considered but dropped: Dalamud's default font atlas does not include FontAwesome codepoints, so mixed-font in a single TabItem label renders as tofu. Underline pill alone is the v1.2.0 visual treatment for top tabs. Resolution would require Font-Atlas merge at FontManager level — out of scope.
## [1.1.0] — 2026-05-05 — Theme Foundation ## [1.1.0] — 2026-05-05 — Theme Foundation
Erster großer UI-Cycle nach v1.0.0. Theme-Engine, fünf Built-In-Themes, Erster großer UI-Cycle nach v1.0.0. Theme-Engine, fünf Built-In-Themes,
+5 -5
View File
File diff suppressed because one or more lines are too long