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]
|
[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.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
|
// 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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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();
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
## [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,
|
||||||
|
|||||||
Reference in New Issue
Block a user