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:
@@ -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.
|
||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 14;
|
||||
private const int LatestVersion = 15;
|
||||
|
||||
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.
|
||||
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.5–1.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
|
||||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
||||
// 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
|
||||
// *this* configuration alive across an UpdateFrom so a settings
|
||||
// save (or sidebar-mode toggle) does not silently destroy the
|
||||
// user's open tell conversations. Persistent tabs from `other`
|
||||
// still get the regular clone-replace treatment.
|
||||
// user's open tell conversations.
|
||||
//
|
||||
// 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();
|
||||
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);
|
||||
|
||||
OverrideStyle = other.OverrideStyle;
|
||||
@@ -336,10 +346,6 @@ public class Configuration : IPluginConfiguration
|
||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||
|
||||
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;
|
||||
|
||||
// v1.1.0 theme engine fields
|
||||
@@ -394,6 +400,11 @@ public class Tab
|
||||
{
|
||||
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")]
|
||||
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>
|
||||
/// Returns an array copy of the message list for usage outside of main thread
|
||||
/// </summary>
|
||||
|
||||
@@ -120,7 +120,16 @@ public class FontManager
|
||||
e => e.OnPreBuild(
|
||||
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
|
||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
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
|
||||
derives from. -->
|
||||
<Version>1.1.0</Version>
|
||||
<Version>1.2.0</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||
|
||||
@@ -55,6 +55,91 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
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**
|
||||
|
||||
First major UI cycle after the standalone v1.0.0 cut. Theme engine,
|
||||
|
||||
@@ -151,7 +151,13 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -165,7 +171,11 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
// We store the pending messages to be added to the chat log in a
|
||||
// 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 (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
||||
pendingMessages.Add(message);
|
||||
|
||||
+19
-3
@@ -64,6 +64,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
internal TypingIpc TypingIpc { get; }
|
||||
internal FontManager FontManager { get; }
|
||||
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
@@ -246,9 +247,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
if (Config.Version < 14)
|
||||
{
|
||||
Config.Theme = "hellion-arctic";
|
||||
#pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0
|
||||
Config.WindowOpacity = Config.HellionThemeWindowOpacity;
|
||||
#pragma warning restore CS0612, CS0618
|
||||
// v1.2.0: alter Opacity-Wert wird nicht mehr migriert (Field entfernt).
|
||||
// User die direkt v13 → v15 springen bekommen den Default 0.85.
|
||||
Config.ReduceMotion = false;
|
||||
Config.UseCompactDensity = false;
|
||||
Config.ShowThemeQuickPicker = false;
|
||||
@@ -259,6 +259,20 @@ public sealed class Plugin : IDalamudPlugin
|
||||
"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
|
||||
// tabs: General catches the immediate-surroundings public chat
|
||||
// (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.Switch(Config.Theme);
|
||||
|
||||
StatusBar = new Ui.StatusBar();
|
||||
|
||||
MessageManager = new MessageManager(this); // Does it require UI?
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||
|
||||
+13
@@ -276,6 +276,11 @@ internal class HellionStrings
|
||||
internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell));
|
||||
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)
|
||||
internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default));
|
||||
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 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));
|
||||
|
||||
// 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">
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<value>Klassik (Chat 2 Default)</value>
|
||||
</data>
|
||||
@@ -705,4 +716,16 @@
|
||||
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||
<value>Behalten</value>
|
||||
</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>
|
||||
|
||||
@@ -561,6 +561,17 @@
|
||||
<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>
|
||||
</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">
|
||||
<value>Klassik (Chat 2 Default)</value>
|
||||
</data>
|
||||
@@ -705,4 +716,16 @@
|
||||
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||
<value>Keep current</value>
|
||||
</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>
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
+192
-38
@@ -375,6 +375,9 @@ public sealed class ChatLogWindow : Window
|
||||
// weil der Cursor schon weiter unten steht — kein eigener Abzug.
|
||||
height -= ImGui.GetFrameHeightWithSpacing();
|
||||
|
||||
// v1.2.0 — Status-Bar am Window-Boden reserviert 22 px + 2 px Spacing.
|
||||
height -= StatusBar.Height + 2;
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
@@ -790,13 +793,17 @@ public sealed class ChatLogWindow : Window
|
||||
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
|
||||
LastActivityTime = FrameTime;
|
||||
|
||||
if (!showNovice)
|
||||
return;
|
||||
if (showNovice)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf))
|
||||
GameFunctions.GameFunctions.ClickNoviceNetworkButton();
|
||||
}
|
||||
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf))
|
||||
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()
|
||||
@@ -1316,17 +1323,63 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var lineWidth = ImGui.GetContentRegionAvail().X;
|
||||
if (message.Sender.Count > 0)
|
||||
{
|
||||
DrawChunks(message.Sender, true, handler, lineWidth);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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
|
||||
DrawChunks(message.Content, true, handler, lineWidth);
|
||||
{
|
||||
if (message.Sender.Count > 0)
|
||||
{
|
||||
DrawChunks(message.Sender, true, handler, lineWidth);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
message.IsVisible[tab.Identifier] = ImGui.IsItemVisible();
|
||||
}
|
||||
@@ -1366,6 +1419,20 @@ public sealed class ChatLogWindow : Window
|
||||
if (!tabItem.Success)
|
||||
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;
|
||||
Plugin.LastTab = tabI;
|
||||
|
||||
@@ -1383,21 +1450,36 @@ public sealed class ChatLogWindow : Window
|
||||
private void DrawTabSidebar()
|
||||
{
|
||||
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)
|
||||
return;
|
||||
|
||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 4);
|
||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f);
|
||||
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var hasTabSwitched = false;
|
||||
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)))
|
||||
{
|
||||
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;
|
||||
// Hellion Chat — auto-tell-tabs section divider rendered
|
||||
// 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 selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}";
|
||||
var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI;
|
||||
|
||||
var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
|
||||
@@ -1457,34 +1538,107 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
bool clicked;
|
||||
if (showGreetedAffordance && tab.IsGreeted)
|
||||
// v1.2.0 — Icon-only Sidebar mit Tooltip beim Hover.
|
||||
// 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
|
||||
// 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);
|
||||
}
|
||||
iconColor = theme.Colors.Accent;
|
||||
}
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
||||
if (clicked)
|
||||
Plugin.WantedTab = tabI;
|
||||
|
||||
if (!clicked && Plugin.WantedTab != tabI)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -210,14 +210,24 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|
||||
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
|
||||
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);
|
||||
|
||||
// save after 60 frames have passed, which should hopefully not
|
||||
// commit any changes that cause a crash
|
||||
Plugin.DeferredSaveFrames = 60;
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabsAsync();
|
||||
if (filtersChanged)
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabsAsync();
|
||||
}
|
||||
|
||||
if (fontChanged || fontSizeChanged || italicStateChanged)
|
||||
Plugin.FontManager.BuildFonts();
|
||||
@@ -233,4 +243,59 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,32 +45,11 @@ internal sealed class Appearance : ISettingsTab
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
// v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten
|
||||
// Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten,
|
||||
// damit die Settings-Seite kompiliert; sie schreiben in die mit
|
||||
// [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net
|
||||
// bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen
|
||||
// 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.5–1.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();
|
||||
|
||||
// v1.2.0 — Legacy HellionThemeEnabled/HellionThemeWindowOpacity-Bindings
|
||||
// entfernt. Theme-Auswahl + globale Window-Opacity leben jetzt in
|
||||
// Settings → Themes (eingeführt mit v1.1.0). Hier verbleibt nur der
|
||||
// klassische OverrideStyle-Toggle plus der Bestand-WindowAlpha-Slider
|
||||
// für das Chat-Log-Fenster.
|
||||
ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle);
|
||||
ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc);
|
||||
|
||||
@@ -79,16 +58,7 @@ internal sealed class Appearance : ISettingsTab
|
||||
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);
|
||||
}
|
||||
#pragma warning restore CS0612, CS0618
|
||||
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +109,22 @@ internal sealed class Appearance : ISettingsTab
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
|
||||
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.Spacing();
|
||||
@@ -356,6 +341,11 @@ internal sealed class Appearance : ISettingsTab
|
||||
ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty);
|
||||
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);
|
||||
ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,40 @@ internal sealed class Tabs : ISettingsTab
|
||||
}
|
||||
|
||||
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_PopOut, ref tab.PopOut);
|
||||
if (tab.PopOut)
|
||||
|
||||
@@ -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("·");
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Erster großer UI-Cycle nach v1.0.0. Theme-Engine, fünf Built-In-Themes,
|
||||
|
||||
Reference in New Issue
Block a user