Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c26d1aa67 | |||
| 8b13ba1fdc | |||
| 52da5d5e23 | |||
| 916640fb60 | |||
| feeb1df4eb | |||
| f2086865ce | |||
| 15a89dd6e7 | |||
| 53952717c0 | |||
| fcbbd174b6 | |||
| d41cea0031 | |||
| c943a2cff3 | |||
| abcd0847ef | |||
| 2f52cbb7d4 | |||
| 9103bbb892 | |||
| 8f9c01d322 | |||
| af4651b37e | |||
| 485dc4e1b4 | |||
| c878d24d11 | |||
| cb5c940a84 | |||
| dd3a0ea069 | |||
| 4bf6c3ef1f | |||
| 2378ce6bf2 | |||
| b85db24601 | |||
| cae7d76206 | |||
| 4c6d52e652 | |||
| cbfdfe35be | |||
| 537b96c79f | |||
| d3d28924e6 | |||
| 48f1fb5ba1 | |||
| 0b13efd0b5 | |||
| 289fe2eb78 | |||
| fe9e66b0ff | |||
| 990edd8300 | |||
| db95ec7dff | |||
| 7e036c1d00 | |||
| 1c511a147d | |||
| f093d93761 | |||
| e7c8667497 | |||
| 497197eb2c | |||
| 08b2ffc600 | |||
| 8db3eca46c | |||
| 4d54eabdac | |||
| 698eb01bbe | |||
| a3fbaab173 | |||
| 57291e925d | |||
| 8e9332ac8c |
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Theme Foundation"
|
||||||
|
versionsnatur: "Major-UI-Cycle"
|
||||||
|
---
|
||||||
|
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove
|
||||||
|
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View, Breadcrumb und ESC zurück zur Übersicht
|
||||||
|
- Themes-Tab mit Mini-Mockup pro Theme, Live-Switch beim Klick
|
||||||
|
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten Start automatisch abgelegt
|
||||||
|
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch überschrieben
|
||||||
|
- Plugin-Icon zum Hellion-Forge-Hammer gewechselt
|
||||||
|
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2 Klassik in Settings → Themes
|
||||||
|
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
||||||
@@ -39,10 +39,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: csharp
|
languages: csharp
|
||||||
build-mode: manual
|
build-mode: manual
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
category: /language:csharp
|
category: /language:csharp
|
||||||
|
|
||||||
@@ -79,15 +79,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: actions
|
languages: actions
|
||||||
build-mode: none
|
build-mode: none
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
category: /language:actions
|
category: /language:actions
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
scripts/
|
scripts/
|
||||||
|
|
||||||
|
# Local test project (stays out of the published plugin repo;
|
||||||
|
# pure-function safety net for refactor cycles)
|
||||||
|
HellionChat.Tests/
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
pack/
|
pack/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||||
|
|
||||||
Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
||||||
Original ChatTwo authors and copyright holders of the upstream
|
Original ChatTwo authors and copyright holders of the upstream
|
||||||
plugin this fork is built on. Their work covers the message store,
|
plugin this fork is built on. Their work covers the message store,
|
||||||
the channel filtering, the sidebar tab system, the FFXIV chat
|
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -34,10 +34,23 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 12;
|
private const int LatestVersion = 14;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
|
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
|
||||||
|
public string Theme = "hellion-arctic";
|
||||||
|
|
||||||
|
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
|
||||||
|
// HellionThemeWindowOpacity beim Bump v13 → v14.
|
||||||
|
public float WindowOpacity = 0.85f;
|
||||||
|
|
||||||
|
// v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
|
||||||
|
// vorab angelegt, damit später keine Migration nötig ist.
|
||||||
|
public bool ReduceMotion;
|
||||||
|
public bool UseCompactDensity;
|
||||||
|
public bool ShowThemeQuickPicker;
|
||||||
|
|
||||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
||||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
// Master-switch defaults to true; set false to restore upstream behavior.
|
||||||
public bool PrivacyFilterEnabled = true;
|
public bool PrivacyFilterEnabled = true;
|
||||||
@@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration
|
|||||||
// Hellion Chat global ImGui theme — applied to every plugin window in
|
// Hellion Chat global ImGui theme — applied to every plugin window in
|
||||||
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
||||||
// can flip this off in the Privacy tab.
|
// can flip this off in the Privacy tab.
|
||||||
|
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
|
||||||
public bool HellionThemeEnabled = true;
|
public bool HellionThemeEnabled = true;
|
||||||
|
|
||||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||||||
// panes more glass-like so the game shines through. Default 0.5
|
// panes more glass-like so the game shines through. Default 0.5
|
||||||
// matches the maintainer's daily-driver preference; users who want
|
// matches the maintainer's daily-driver preference; users who want
|
||||||
// a less translucent look bump it up in Aussehen → Theme.
|
// a less translucent look bump it up in Aussehen → Theme.
|
||||||
|
[Obsolete("Replaced by WindowOpacity in v14")]
|
||||||
public float HellionThemeWindowOpacity = 0.5f;
|
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
|
||||||
@@ -145,6 +160,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool HideWhenUiHidden = true;
|
public bool HideWhenUiHidden = true;
|
||||||
public bool HideInLoadingScreens;
|
public bool HideInLoadingScreens;
|
||||||
public bool HideInBattle;
|
public bool HideInBattle;
|
||||||
|
public bool HideInNewGamePlusMenu;
|
||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
public int InactivityHideTimeout = 10;
|
public int InactivityHideTimeout = 10;
|
||||||
public bool InactivityHideActiveDuringBattle = true;
|
public bool InactivityHideActiveDuringBattle = true;
|
||||||
@@ -221,6 +237,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public float TooltipOffset;
|
public float TooltipOffset;
|
||||||
public float WindowAlpha = 100f;
|
public float WindowAlpha = 100f;
|
||||||
public Dictionary<ChatType, uint> ChatColours = new();
|
public Dictionary<ChatType, uint> ChatColours = new();
|
||||||
|
public bool ColorSelectedInputChannelButton = true;
|
||||||
public List<Tab> Tabs = [];
|
public List<Tab> Tabs = [];
|
||||||
|
|
||||||
public bool OverrideStyle;
|
public bool OverrideStyle;
|
||||||
@@ -241,6 +258,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
HideWhenUiHidden = other.HideWhenUiHidden;
|
HideWhenUiHidden = other.HideWhenUiHidden;
|
||||||
HideInLoadingScreens = other.HideInLoadingScreens;
|
HideInLoadingScreens = other.HideInLoadingScreens;
|
||||||
HideInBattle = other.HideInBattle;
|
HideInBattle = other.HideInBattle;
|
||||||
|
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
|
||||||
HideWhenInactive = other.HideWhenInactive;
|
HideWhenInactive = other.HideWhenInactive;
|
||||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
InactivityHideTimeout = other.InactivityHideTimeout;
|
||||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||||||
@@ -276,7 +294,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
ShowEmotes = other.ShowEmotes;
|
ShowEmotes = other.ShowEmotes;
|
||||||
BlockedEmotes = other.BlockedEmotes;
|
// Deep-copy the set so the live and mutable Configuration instances don't share state
|
||||||
|
// — a HashSet reference assignment would cause edits in the settings window to leak
|
||||||
|
// into the live config before the user clicks Save.
|
||||||
|
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||||||
FontsEnabled = other.FontsEnabled;
|
FontsEnabled = other.FontsEnabled;
|
||||||
ItalicEnabled = other.ItalicEnabled;
|
ItalicEnabled = other.ItalicEnabled;
|
||||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||||||
@@ -288,6 +309,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
TooltipOffset = other.TooltipOffset;
|
TooltipOffset = other.TooltipOffset;
|
||||||
WindowAlpha = other.WindowAlpha;
|
WindowAlpha = other.WindowAlpha;
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||||
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
||||||
// 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
|
||||||
@@ -314,10 +336,19 @@ 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;
|
HellionThemeEnabled = other.HellionThemeEnabled;
|
||||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||||||
|
#pragma warning restore CS0612, CS0618
|
||||||
UseHellionFont = other.UseHellionFont;
|
UseHellionFont = other.UseHellionFont;
|
||||||
|
|
||||||
|
// v1.1.0 theme engine fields
|
||||||
|
Theme = other.Theme;
|
||||||
|
WindowOpacity = other.WindowOpacity;
|
||||||
|
ReduceMotion = other.ReduceMotion;
|
||||||
|
UseCompactDensity = other.UseCompactDensity;
|
||||||
|
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
||||||
|
|
||||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||||
|
|||||||
@@ -66,16 +66,29 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public static string[] SortedCodeArray = [];
|
public static string[] SortedCodeArray = [];
|
||||||
|
|
||||||
|
// Plugin-scoped cancellation source for in-flight emote loads. Dispose
|
||||||
|
// cancels every running download/texture-create so the workers don't
|
||||||
|
// touch a torn-down TextureProvider on plugin reload. Replaced with a
|
||||||
|
// fresh source on the next LoadData() call so a re-enable still works.
|
||||||
|
private static CancellationTokenSource Cts = new();
|
||||||
|
internal static CancellationToken Token => Cts.Token;
|
||||||
|
|
||||||
public static async Task LoadData()
|
public static async Task LoadData()
|
||||||
{
|
{
|
||||||
if (State is not LoadingState.Unloaded)
|
if (State is not LoadingState.Unloaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Refresh the CTS in case Dispose was called and we're being re-enabled
|
||||||
|
// in the same process (Dalamud /xlplugins toggle).
|
||||||
|
if (Cts.IsCancellationRequested)
|
||||||
|
Cts = new CancellationTokenSource();
|
||||||
|
|
||||||
State = LoadingState.Loading;
|
State = LoadingState.Loading;
|
||||||
|
var ct = Cts.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var global = await Client.GetAsync(GlobalEmotes);
|
var global = await Client.GetAsync(GlobalEmotes, ct);
|
||||||
var globalList = await global.Content.ReadAsStringAsync();
|
var globalList = await global.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
|
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
|
||||||
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
|
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
|
||||||
@@ -84,8 +97,8 @@ public static class EmoteCache
|
|||||||
var lastId = string.Empty;
|
var lastId = string.Empty;
|
||||||
for (var i = 0; i < 15; i++)
|
for (var i = 0; i < 15; i++)
|
||||||
{
|
{
|
||||||
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId));
|
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId), ct);
|
||||||
var topList = await top.Content.ReadAsStringAsync();
|
var topList = await top.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
||||||
// BetterTTV occasionally returns entries with a null Code; the
|
// BetterTTV occasionally returns entries with a null Code; the
|
||||||
@@ -103,6 +116,12 @@ public static class EmoteCache
|
|||||||
SortedCodeArray = Cache.Keys.Order().ToArray();
|
SortedCodeArray = Cache.Keys.Order().ToArray();
|
||||||
State = LoadingState.Done;
|
State = LoadingState.Done;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed while the cache was loading; leave State on
|
||||||
|
// Loading so a subsequent re-enable can re-issue LoadData with
|
||||||
|
// a fresh CTS (handled above).
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
||||||
@@ -116,6 +135,10 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public static void Dispose()
|
public static void Dispose()
|
||||||
{
|
{
|
||||||
|
// Cancel in-flight downloads / texture creates so the async-void
|
||||||
|
// Load methods bail out before they touch a disposed TextureProvider.
|
||||||
|
Cts.Cancel();
|
||||||
|
|
||||||
foreach (var emote in EmoteImages.Values)
|
foreach (var emote in EmoteImages.Values)
|
||||||
emote.InnerDispose();
|
emote.InnerDispose();
|
||||||
}
|
}
|
||||||
@@ -171,7 +194,7 @@ public static class EmoteCache
|
|||||||
ImGui.Image(Texture!.Handle, size);
|
ImGui.Image(Texture!.Handle, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<byte[]> LoadAsync(Emote emote)
|
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// BetterTTV-supplied Id and ImageType are interpolated straight
|
// BetterTTV-supplied Id and ImageType are interpolated straight
|
||||||
// into the filename. HTTPS protects the wire, but a compromised
|
// into the filename. HTTPS protects the wire, but a compromised
|
||||||
@@ -188,15 +211,15 @@ public static class EmoteCache
|
|||||||
|
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
RawData = await File.ReadAllBytesAsync(filePath);
|
RawData = await File.ReadAllBytesAsync(filePath, ct);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var content = await Client.GetAsync(EmotePath.Format(emote.Id));
|
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
||||||
RawData = await content.Content.ReadAsByteArrayAsync();
|
RawData = await content.Content.ReadAsByteArrayAsync(ct);
|
||||||
|
|
||||||
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||||
stream.Write(RawData, 0, RawData.Length);
|
await stream.WriteAsync(RawData, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RawData;
|
return RawData;
|
||||||
@@ -209,21 +232,28 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
public ImGuiEmote Prepare(Emote emote)
|
public ImGuiEmote Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
Task.Run(() => Load(emote));
|
var ct = EmoteCache.Token;
|
||||||
|
Task.Run(() => Load(emote, ct), ct);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote)
|
private async void Load(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await LoadAsync(emote);
|
var image = await LoadAsync(emote, ct);
|
||||||
if (image.Length <= 0)
|
if (image.Length <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image);
|
ct.ThrowIfCancellationRequested();
|
||||||
|
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct);
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed mid-load; the EmoteImages entry is also
|
||||||
|
// being torn down, no extra cleanup needed.
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
@@ -279,15 +309,16 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public ImGuiGif Prepare(Emote emote)
|
public ImGuiGif Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
Task.Run(() => Load(emote));
|
var ct = EmoteCache.Token;
|
||||||
|
Task.Run(() => Load(emote, ct), ct);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote)
|
private async void Load(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await LoadAsync(emote);
|
var image = await LoadAsync(emote, ct);
|
||||||
if (image.Length <= 0)
|
if (image.Length <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -299,6 +330,8 @@ public static class EmoteCache
|
|||||||
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
||||||
foreach (var frame in img.Frames)
|
foreach (var frame in img.Frames)
|
||||||
{
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||||
|
|
||||||
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
||||||
@@ -307,13 +340,21 @@ public static class EmoteCache
|
|||||||
|
|
||||||
var buffer = new byte[4 * frame.Width * frame.Height];
|
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||||
frame.CopyPixelDataTo(buffer);
|
frame.CopyPixelDataTo(buffer);
|
||||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer);
|
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct);
|
||||||
frames.Add((tex, delay));
|
frames.Add((tex, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
Frames = frames;
|
Frames = frames;
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed mid-load; partial frames are released by
|
||||||
|
// InnerDispose on the next dispose pass.
|
||||||
|
foreach (var f in Frames)
|
||||||
|
f.Texture.Dispose();
|
||||||
|
Frames = [];
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public class FontManager
|
|||||||
|
|
||||||
internal IFontHandle FontAwesome = null!;
|
internal IFontHandle FontAwesome = null!;
|
||||||
|
|
||||||
internal readonly byte[] GameSymFont;
|
|
||||||
|
|
||||||
private ushort[] Ranges = [];
|
private ushort[] Ranges = [];
|
||||||
private ushort[] JpRange = [];
|
private ushort[] JpRange = [];
|
||||||
|
|
||||||
@@ -30,32 +28,6 @@ public class FontManager
|
|||||||
36f, 40f, 45f, 46f, 68f, 90f,
|
36f, 40f, 45f, 46f, 68f, 90f,
|
||||||
];
|
];
|
||||||
|
|
||||||
public FontManager()
|
|
||||||
{
|
|
||||||
var filePath = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "FFXIV_Lodestone_SSF.ttf");
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
GameSymFont = File.ReadAllBytes(filePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Dispose HttpClient and HttpResponseMessage to avoid socket
|
|
||||||
// exhaustion on repeated cold-start downloads. GetAwaiter().GetResult()
|
|
||||||
// unwraps AggregateException so failures surface cleanly. A full
|
|
||||||
// async refactor of the constructor would be cleaner but is out of
|
|
||||||
// scope for v1.0.0 — tracked in the backlog.
|
|
||||||
using var client = new HttpClient();
|
|
||||||
using var response = client
|
|
||||||
.GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
GameSymFont = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
|
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
|
||||||
/// extracted from the assembly's manifest resources on first use; the
|
/// extracted from the assembly's manifest resources on first use; the
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||||
worldId = agent->TellWorldId;
|
worldId = agent->TellWorldId;
|
||||||
Plugin.Log.Debug($"Detected tell target '{playerName}'@{worldId}");
|
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||||
@@ -400,7 +400,9 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
||||||
return channel + idx;
|
// RotateLinkshell returns null when no valid linkshell is found within 8 iterations.
|
||||||
|
// Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic.
|
||||||
|
return idx is null ? null : channel + idx.Value;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return channel;
|
return channel;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ namespace HellionChat.GameFunctions;
|
|||||||
|
|
||||||
internal unsafe class GameFunctions : IDisposable
|
internal unsafe class GameFunctions : IDisposable
|
||||||
{
|
{
|
||||||
|
internal const string NewGamePlusAddonName = "QuestRedo";
|
||||||
|
|
||||||
#region Hooks
|
#region Hooks
|
||||||
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
||||||
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
||||||
@@ -243,7 +245,8 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
vf0(agent, &result, &value, 0, 0);
|
vf0(agent, &result, &value, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(128);
|
private const int PlaceholderBufferSize = 128;
|
||||||
|
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize);
|
||||||
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
||||||
private string? ReplacementName;
|
private string? ReplacementName;
|
||||||
|
|
||||||
@@ -259,6 +262,17 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
if (ReplacementName == null || placeholder != Placeholder)
|
if (ReplacementName == null || placeholder != Placeholder)
|
||||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||||
|
|
||||||
|
// The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit.
|
||||||
|
// FFXIV player names plus an @World suffix should never approach this
|
||||||
|
// limit, but a malformed ReplacementName must not overflow the buffer.
|
||||||
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||||
|
if (byteCount >= PlaceholderBufferSize)
|
||||||
|
{
|
||||||
|
Plugin.Log.Warning($"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original.");
|
||||||
|
ReplacementName = null;
|
||||||
|
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||||
|
}
|
||||||
|
|
||||||
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class TellTarget
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSet()
|
public bool IsSet()
|
||||||
=> Name.Length > 0 && World > 0;
|
=> !string.IsNullOrEmpty(Name) && World > 0;
|
||||||
|
|
||||||
public string ToWorldString()
|
public string ToWorldString()
|
||||||
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
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.0.1</Version>
|
<Version>1.1.0</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||||
don't silently drift between machines or CI runs. -->
|
don't silently drift between machines or CI runs. -->
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
@@ -18,7 +19,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<!-- Closed ranges on packages with breaking-change history block a
|
||||||
|
surprise major bump when the lock file is regenerated. The
|
||||||
|
lock file pins the exact version per build; the upper bound
|
||||||
|
keeps the unlock path from drifting across major lines. -->
|
||||||
|
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
<!-- Override the transitively-referenced native SQLite build to one
|
<!-- Override the transitively-referenced native SQLite build to one
|
||||||
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
||||||
@@ -28,8 +33,15 @@
|
|||||||
without a major bump on the managed wrapper. -->
|
without a major bump on the managed wrapper. -->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
<PackageReference Include="Pidgin" Version="3.5.1" />
|
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Pure-function test suites in HellionChat.Tests need access to
|
||||||
|
the internal helper classes (StringUtil, UriPayload, Tokenizer
|
||||||
|
etc.). Test assembly does not get redistributed. -->
|
||||||
|
<InternalsVisibleTo Include="HellionChat.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -63,6 +75,9 @@
|
|||||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
||||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
|
||||||
|
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ description: |-
|
|||||||
- Independent plugin state — own config file and database directory,
|
- Independent plugin state — own config file and database directory,
|
||||||
so Hellion Chat does not share state with upstream Chat 2
|
so Hellion Chat does not share state with upstream Chat 2
|
||||||
|
|
||||||
|
v1.1.0 — Theme engine with five built-in themes (Hellion Arctic,
|
||||||
|
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) plus
|
||||||
|
JSON-based custom-theme authoring. Settings rebuilt around a card
|
||||||
|
grid with section detail views. See docs/THEME-AUTHORING.md.
|
||||||
|
|
||||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||||
|
|
||||||
Modding & support: join the Hellion Forge Discord at
|
Modding & support: join the Hellion Forge Discord at
|
||||||
@@ -41,7 +46,8 @@ accepts_feedback: true
|
|||||||
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
||||||
image_urls:
|
image_urls:
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/withSimpleTweaks.png
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png
|
||||||
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png
|
||||||
tags:
|
tags:
|
||||||
- Social
|
- Social
|
||||||
- UI
|
- UI
|
||||||
@@ -49,6 +55,82 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
|
**Hellion Chat 1.1.0 — Theme Foundation**
|
||||||
|
|
||||||
|
First major UI cycle after the standalone v1.0.0 cut. Theme engine,
|
||||||
|
five built-in themes, customisable JSON themes, modernised settings
|
||||||
|
layout.
|
||||||
|
|
||||||
|
New themes (Settings → Themes):
|
||||||
|
|
||||||
|
- **Hellion Arctic** — the brand default, Arctic Cyan + Ember Glow
|
||||||
|
on industrial slate.
|
||||||
|
- **Chat 2 Klassik** — Steel Blue on neutral grey, eckige Kanten.
|
||||||
|
The upstream Chat 2 look on the new engine.
|
||||||
|
- **Event Horizon** — Cosmic Purple on near-black. Deep-space mood.
|
||||||
|
- **Moonlit Bloom** — Bloom Magenta + Soft Sage on deep-violet
|
||||||
|
night.
|
||||||
|
- **Mint Grove** — Mint Green + Honey Amber on deep forest. First
|
||||||
|
member of the Grove family.
|
||||||
|
|
||||||
|
Theme engine highlights:
|
||||||
|
|
||||||
|
- Slug-based selection in Settings → Themes with mini-mockup
|
||||||
|
previews per theme.
|
||||||
|
- Click a theme card and the whole plugin (chat, settings,
|
||||||
|
pop-outs, viewer) repaints instantly.
|
||||||
|
- Custom themes via JSON in pluginConfigs/HellionChat/themes/.
|
||||||
|
Example template seeded on first launch.
|
||||||
|
- Optional per-theme chat-channel colours. When a theme proposes
|
||||||
|
its own chat colours and yours differ, a dezent banner offers
|
||||||
|
Apply / Keep — never auto-overwriting.
|
||||||
|
- Migration v13 → v14: existing users land on Hellion Arctic. Pick
|
||||||
|
Chat 2 Klassik to keep the upstream look.
|
||||||
|
|
||||||
|
Settings layout:
|
||||||
|
|
||||||
|
- New card-grid overview on Settings open. Click a card to drill
|
||||||
|
into the section.
|
||||||
|
- Breadcrumb back to overview, ESC also returns.
|
||||||
|
- Detail view drops the redundant tab list — section content uses
|
||||||
|
the full width.
|
||||||
|
|
||||||
|
Branding:
|
||||||
|
|
||||||
|
- Plugin icon swapped from the ChatTwo derivative to the Hellion
|
||||||
|
Forge hammer.
|
||||||
|
- New docs/THEME-AUTHORING.md walks you through writing your own
|
||||||
|
themes with the Forge logo on top.
|
||||||
|
|
||||||
|
Technical:
|
||||||
|
|
||||||
|
- HellionStyle.PushGlobal is now theme-driven. Configuration.
|
||||||
|
HellionThemeEnabled is deprecated and will be removed in v1.2.0.
|
||||||
|
- New ThemeRegistry singleton with LastWriteTime-cached custom-
|
||||||
|
theme loader.
|
||||||
|
- 51 local unit tests cover the data model, registry, JSON round-
|
||||||
|
trip and built-in sanity checks.
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
**Hellion Chat 1.0.3 — Polish patch**
|
||||||
|
|
||||||
|
- New: optionally hide chat (and every other plugin window) while the
|
||||||
|
New Game+ menu is open. Toggle in Settings → Window → Frame, default
|
||||||
|
off. Closing the menu restores all windows.
|
||||||
|
- New: optionally tint the channel selector button next to the input
|
||||||
|
field with the currently active channel's colour. Toggle in
|
||||||
|
Settings → Appearance → Colours, default on. Matches the existing
|
||||||
|
input-text tint and respects ExtraChat overrides.
|
||||||
|
- Fix: status, item and other inline hover icons keep their original
|
||||||
|
aspect ratio. Debuff icons with non-square dimensions are no longer
|
||||||
|
visually squished into a 32×32 box.
|
||||||
|
- Diagnostic: hide-state transitions (battle, cutscene, user-hide,
|
||||||
|
cutscene override) are now logged on Verbose level for easier bug
|
||||||
|
reports — off by default, enable with `/xllog set HellionChat verbose`.
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 1.0.1 — Window Position Recovery**
|
**Hellion Chat 1.0.1 — Window Position Recovery**
|
||||||
|
|
||||||
- Automatic bounds check on the first draw after plugin load.
|
- Automatic bounds check on the first draw after plugin load.
|
||||||
@@ -214,76 +296,6 @@ changelog: |-
|
|||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**
|
|
||||||
|
|
||||||
Two opt-in UX features land in the same release. Existing users see
|
|
||||||
no change unless they enable the new toggles.
|
|
||||||
|
|
||||||
Pop-out input bar:
|
|
||||||
|
|
||||||
- New global master switch in Settings → Window → Frame: "Enable input
|
|
||||||
in pop-outs". Default OFF so existing behaviour is preserved
|
|
||||||
- When enabled, every pop-out window grows a compact input bar at the
|
|
||||||
bottom (channel-coloured icon button left, text input right). The
|
|
||||||
auto-translate picker is intentionally not part of the compact bar
|
|
||||||
in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)
|
|
||||||
rarely need it there
|
|
||||||
- Each pop-out keeps an independent text buffer and history cursor;
|
|
||||||
channel changes still apply globally because that is how the FFXIV
|
|
||||||
channel API works
|
|
||||||
- Up/Down navigates a shared input history singleton across the main
|
|
||||||
window and every open pop-out
|
|
||||||
- First pop-out opening after the upgrade shows a one-time hint
|
|
||||||
banner pointing users to the new toggle
|
|
||||||
|
|
||||||
Chat colour presets:
|
|
||||||
|
|
||||||
- Seven built-in presets above the per-channel colour list in
|
|
||||||
Settings → Appearance → Colours: ChatTwo Default, High-Contrast,
|
|
||||||
Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange
|
|
||||||
Arctic Cyan + Ember Glow palette from the Hellion Online Media
|
|
||||||
branding spec), plus two bonus mood presets — Night Blue (royal
|
|
||||||
blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)
|
|
||||||
- Apply is immediate and overwrites the channels covered by the
|
|
||||||
preset; battle-channel colours are left alone so combat tuning
|
|
||||||
stays intact
|
|
||||||
|
|
||||||
Configuration migrates from v10 to v11 with a diagnostic log entry;
|
|
||||||
no data is reset. Bilingual (English/German) for both new sections.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.4 — WrapText hardening**
|
|
||||||
|
|
||||||
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
|
||||||
Span- and index-based control flow. Closes the persistent CodeQL
|
|
||||||
Critical alert "unvalidated local pointer arithmetic" that kept
|
|
||||||
re-firing on every shape of the previous fix.
|
|
||||||
|
|
||||||
Hardening:
|
|
||||||
|
|
||||||
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
|
|
||||||
via ArrayPool, validates the actual encoded length against that
|
|
||||||
ceiling, and threads the rest of the algorithm through int offsets
|
|
||||||
instead of raw byte pointers
|
|
||||||
- Pointer arithmetic only happens inside two small private helpers
|
|
||||||
(CalcWordWrap and DrawText) that take the pinned base pointer plus
|
|
||||||
int offsets sourced from the plugin's own logic, not from any
|
|
||||||
virtual-method return
|
|
||||||
- Added a 16 KiB upper bound on the buffer rent to prevent a
|
|
||||||
pathological input from triggering an unbounded ArrayPool allocation
|
|
||||||
|
|
||||||
No user-visible behaviour change. Word-wrap output is byte-identical
|
|
||||||
to v0.5.3.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
|
||||||
|
|
||||||
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
|
||||||
encoded byte buffer length via GetByteCount before pointer
|
|
||||||
arithmetic. Single-fix patch on top of v0.5.2.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ public sealed class ExtraChat : IDisposable
|
|||||||
|
|
||||||
internal (string, uint)? ChannelOverride { get; set; }
|
internal (string, uint)? ChannelOverride { get; set; }
|
||||||
|
|
||||||
private Dictionary<string, uint> ChannelCommandColoursInternal { get; set; } = new();
|
// Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a
|
||||||
|
// Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections.
|
||||||
|
// Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
|
||||||
|
// the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
|
||||||
|
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
||||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
||||||
|
|
||||||
private Dictionary<Guid, string> ChannelNamesInternal { get; set; } = new();
|
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
||||||
|
|
||||||
internal ExtraChat()
|
internal ExtraChat()
|
||||||
@@ -40,9 +44,10 @@ public sealed class ExtraChat : IDisposable
|
|||||||
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
||||||
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
|
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// no-op
|
// ExtraChat is optional; missing IPC peer is normal when the plugin isn't loaded.
|
||||||
|
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// After that, the message is enqueued in the PendingAsync queue, which will
|
// After that, the message is enqueued in the PendingAsync queue, which will
|
||||||
// be consumed in a separate thread and perform more processing (emotes,
|
// be consumed in a separate thread and perform more processing (emotes,
|
||||||
// URLs) as well as inserting the message into the database.
|
// URLs) as well as inserting the message into the database.
|
||||||
private Queue<PendingMessage> PendingSync { get; } = [];
|
// LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
|
||||||
|
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
|
||||||
|
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
|
||||||
|
private LinkedList<PendingMessage> PendingSync { get; } = [];
|
||||||
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
||||||
private readonly Thread PendingMessageThread;
|
private readonly Thread PendingMessageThread;
|
||||||
private readonly CancellationTokenSource PendingThreadCancellationToken = new();
|
private readonly CancellationTokenSource PendingThreadCancellationToken = new();
|
||||||
@@ -93,6 +96,10 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
|
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancellationTokenSource owns an unmanaged WaitHandle; dispose after the
|
||||||
|
// worker thread has drained, otherwise it leaks across plugin reloads.
|
||||||
|
PendingThreadCancellationToken.Dispose();
|
||||||
|
|
||||||
Store.Dispose();
|
Store.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +120,11 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
LastContentId = contentId;
|
LastContentId = contentId;
|
||||||
|
|
||||||
// Drain the PendingSync queue into the PendingAsync queue.
|
// Drain the PendingSync queue into the PendingAsync queue.
|
||||||
while (PendingSync.TryDequeue(out var pending))
|
while (PendingSync.First is { } first)
|
||||||
PendingAsync.Enqueue(pending);
|
{
|
||||||
|
PendingSync.RemoveFirst();
|
||||||
|
PendingAsync.Enqueue(first.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessPendingMessages(CancellationToken token)
|
private void ProcessPendingMessages(CancellationToken token)
|
||||||
@@ -219,7 +229,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// We delay messages to be handed off to the async processing thread
|
// We delay messages to be handed off to the async processing thread
|
||||||
// in the next tick, otherwise we can't get the content ID from the hook
|
// in the next tick, otherwise we can't get the content ID from the hook
|
||||||
// below.
|
// below.
|
||||||
PendingSync.Enqueue(pendingMessage);
|
PendingSync.AddLast(pendingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This hook is called immediately after receiving a message with the
|
// This hook is called immediately after receiving a message with the
|
||||||
@@ -231,11 +241,11 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
||||||
if (PendingSync.Count == 0)
|
if (PendingSync.Last is not { } last)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PendingSync.Last().ContentId = contentId;
|
last.Value.ContentId = contentId;
|
||||||
PendingSync.Last().AccountId = accountId;
|
last.Value.AccountId = accountId;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -452,7 +452,10 @@ internal class MessageStore : IDisposable
|
|||||||
// covers any future write paths e.g. webinterface backfill).
|
// covers any future write paths e.g. webinterface backfill).
|
||||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
// Verbose-only: this fires for every dropped message, which is
|
||||||
|
// the common case for users with a tight privacy whitelist. Keep
|
||||||
|
// it for diagnostics but stay out of the default xllog stream.
|
||||||
|
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,10 +332,19 @@ public sealed class PayloadHandler
|
|||||||
atkBase->SetPosition((short) x, (short) y);
|
atkBase->SetPosition((short) x, (short) y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const float MaxInlineIconSize = 32f;
|
||||||
|
|
||||||
private static void InlineIcon(IDalamudTextureWrap icon)
|
private static void InlineIcon(IDalamudTextureWrap icon)
|
||||||
{
|
{
|
||||||
|
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var width = (float) icon.Size.X;
|
||||||
|
var height = (float) icon.Size.Y;
|
||||||
|
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
|
||||||
|
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
|
||||||
|
|
||||||
var cursor = ImGui.GetCursorPos();
|
var cursor = ImGui.GetCursorPos();
|
||||||
var size = ImGuiHelpers.ScaledVector2(32, 32);
|
|
||||||
ImGui.Image(icon.Handle, size);
|
ImGui.Image(icon.Handle, size);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
internal ExtraChat ExtraChat { get; }
|
internal ExtraChat ExtraChat { get; }
|
||||||
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 int DeferredSaveFrames = -1;
|
internal int DeferredSaveFrames = -1;
|
||||||
|
|
||||||
@@ -237,6 +238,27 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v13 → v14 — theme-engine migration. Alle User landen
|
||||||
|
// auf "hellion-arctic" als neues Default-Theme; die alte
|
||||||
|
// HellionThemeEnabled-Flag wird deprecated und nur noch ein Release
|
||||||
|
// als Safety-Net im JSON behalten. Window-Opacity wandert von
|
||||||
|
// HellionThemeWindowOpacity in das neue WindowOpacity-Feld.
|
||||||
|
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
|
||||||
|
Config.ReduceMotion = false;
|
||||||
|
Config.UseCompactDensity = false;
|
||||||
|
Config.ShowThemeQuickPicker = false;
|
||||||
|
Config.Version = 14;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " +
|
||||||
|
"pick chat2-classic in Settings → Themes for the upstream look");
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -266,6 +288,14 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
ExtraChat = new ExtraChat();
|
ExtraChat = new ExtraChat();
|
||||||
FontManager = new FontManager();
|
FontManager = new FontManager();
|
||||||
|
|
||||||
|
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
||||||
|
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
||||||
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(customThemesDir);
|
||||||
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
|
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
||||||
|
ThemeRegistry.Switch(Config.Theme);
|
||||||
|
|
||||||
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
|
||||||
@@ -529,10 +559,15 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||||
|
// Run the clear+refilter synchronously on the framework thread.
|
||||||
|
// Earlier this called FilterAllTabsAsync(), which is fire-and-forget
|
||||||
|
// — the .Wait() here would return as soon as the inner Task.Run was
|
||||||
|
// dispatched, racing the next sweep cycle against the still-running
|
||||||
|
// filter pass. See AUDIT-2026-05-05 [QUAL-02].
|
||||||
Framework.Run(() =>
|
Framework.Run(() =>
|
||||||
{
|
{
|
||||||
MessageManager.ClearAllTabs();
|
MessageManager.ClearAllTabs();
|
||||||
MessageManager.FilterAllTabsAsync();
|
MessageManager.FilterAllTabs();
|
||||||
}).Wait();
|
}).Wait();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -554,13 +589,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
private void Draw()
|
private void Draw()
|
||||||
{
|
{
|
||||||
// Hellion theme is pushed once per frame here so every plugin window
|
// Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
|
||||||
// (chat log, settings, viewers, wizard, file dialog) renders with
|
// Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
|
||||||
// the same palette. Skipping the push leaves the upstream Dalamud
|
// pro Frame aus der Registry gelesen.
|
||||||
// look untouched for users who flipped the toggle off.
|
using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity);
|
||||||
using IDisposable? _style = Config.HellionThemeEnabled
|
|
||||||
? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
ChatLogWindow.BeginFrame();
|
ChatLogWindow.BeginFrame();
|
||||||
|
|
||||||
@@ -571,6 +603,16 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is
|
||||||
|
// open. Hides every plugin window in one shot (chat log, pop-outs,
|
||||||
|
// settings, db viewer, etc.), matching the LoadingScreens pattern.
|
||||||
|
if (Config.HideInNewGamePlusMenu && GameFunctions.GameFunctions.IsAddonInteractable(GameFunctions.GameFunctions.NewGamePlusAddonName))
|
||||||
|
{
|
||||||
|
ChatLogWindow.FinalizeFrame();
|
||||||
|
TypingIpc.Update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ChatLogWindow.HideStateCheck();
|
ChatLogWindow.HideStateCheck();
|
||||||
|
|
||||||
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
||||||
@@ -635,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
||||||
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
||||||
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
|
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
|
||||||
|
|
||||||
|
// v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded
|
||||||
|
// example-theme.json als Vorlage rein. Bestehende User-Customs werden
|
||||||
|
// nicht angefasst (existing JSONs lassen den Block überspringen).
|
||||||
|
private static void SeedExampleThemeIfEmpty(string dir)
|
||||||
|
{
|
||||||
|
if (Directory.EnumerateFiles(dir, "*.json").Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var examplePath = Path.Combine(dir, "example-theme.json");
|
||||||
|
var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json");
|
||||||
|
if (resourceStream is null)
|
||||||
|
{
|
||||||
|
Log.Warning("Themes example template not found in assembly resources; skipping seed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fileStream = File.Create(examplePath);
|
||||||
|
resourceStream.CopyTo(fileStream);
|
||||||
|
Log.Information($"Seeded example-theme.json into {dir}");
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
resourceStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,37 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
|
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
|
||||||
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
||||||
|
|
||||||
|
// v1.1.0 — Settings card-grid overview
|
||||||
|
internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title));
|
||||||
|
internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext));
|
||||||
|
internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title));
|
||||||
|
internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext));
|
||||||
|
internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title));
|
||||||
|
internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext));
|
||||||
|
internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title));
|
||||||
|
internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext));
|
||||||
|
internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title));
|
||||||
|
internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext));
|
||||||
|
internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title));
|
||||||
|
internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext));
|
||||||
|
internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title));
|
||||||
|
internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext));
|
||||||
|
internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title));
|
||||||
|
internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext));
|
||||||
|
internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title));
|
||||||
|
internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext));
|
||||||
|
|
||||||
|
// v1.1.0 — Themes-Settings-Tab
|
||||||
|
internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes));
|
||||||
|
internal static string Settings_Themes_Active => Get(nameof(Settings_Themes_Active));
|
||||||
|
internal static string Settings_Themes_BuiltIns => Get(nameof(Settings_Themes_BuiltIns));
|
||||||
|
internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom));
|
||||||
|
internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder));
|
||||||
|
internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Hint => Get(nameof(Settings_Themes_ApplyChatColors_Hint));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep));
|
||||||
|
|
||||||
// Hellion Chat — General-Tab section headings
|
// Hellion Chat — General-Tab section headings
|
||||||
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
|
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
|
||||||
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
|
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
|
||||||
|
|||||||
@@ -624,4 +624,85 @@
|
|||||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||||
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
|
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Card_General_Title" xml:space="preserve">
|
||||||
|
<value>Allgemein</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
||||||
|
<value>Sprache und grundlegendes Verhalten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
||||||
|
<value>Erscheinungsbild</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
|
||||||
|
<value>Fensterdeckkraft, Schriften, Bewegung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Title" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
|
||||||
|
<value>Theme wählen oder eigenes importieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Title" xml:space="preserve">
|
||||||
|
<value>Fenster</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
||||||
|
<value>Fensterposition, Rahmen, Hide-Zustände</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
||||||
|
<value>Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
||||||
|
<value>Chat-Verhalten, Emotes, Auto-Tells</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
||||||
|
<value>Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
||||||
|
<value>Tab-Layout, Kanäle, eigene Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
||||||
|
<value>Datenschutz</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
||||||
|
<value>Filter, Aufbewahrung, Bereinigung, Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
||||||
|
<value>Datenbank</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
|
||||||
|
<value>Speicher, Migration, alte Bereinigung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Title" xml:space="preserve">
|
||||||
|
<value>Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
||||||
|
<value>Über, Mitwirkende, Support</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Tab_Themes" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Active" xml:space="preserve">
|
||||||
|
<value>Aktiv: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
|
||||||
|
<value>Eingebaute Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Custom" xml:space="preserve">
|
||||||
|
<value>Eigene Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
|
||||||
|
<value>Themes-Ordner öffnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ExportActive" xml:space="preserve">
|
||||||
|
<value>Aktives exportieren...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
|
||||||
|
<value>Dieses Theme schlägt eigene Channel-Farben vor.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
|
||||||
|
<value>Übernehmen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||||
|
<value>Behalten</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -624,4 +624,85 @@
|
|||||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||||
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
|
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Card_General_Title" xml:space="preserve">
|
||||||
|
<value>General</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
||||||
|
<value>Language and basic behaviour</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
||||||
|
<value>Appearance</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
|
||||||
|
<value>Window opacity, fonts, motion</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Title" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
|
||||||
|
<value>Choose a theme or import your own</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Title" xml:space="preserve">
|
||||||
|
<value>Window</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
||||||
|
<value>Window position, frame, hide states</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
||||||
|
<value>Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
||||||
|
<value>Chat behaviour, emotes, auto-tells</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
||||||
|
<value>Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
||||||
|
<value>Tab layout, channels, custom tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
||||||
|
<value>Privacy</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
||||||
|
<value>Filter, retention, cleanup, export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
||||||
|
<value>Database</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
|
||||||
|
<value>Storage, migration, legacy cleanup</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Title" xml:space="preserve">
|
||||||
|
<value>Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
||||||
|
<value>About, credits, support</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Tab_Themes" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Active" xml:space="preserve">
|
||||||
|
<value>Active: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
|
||||||
|
<value>Built-in themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Custom" xml:space="preserve">
|
||||||
|
<value>Custom themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
|
||||||
|
<value>Open themes folder</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ExportActive" xml:space="preserve">
|
||||||
|
<value>Export active...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
|
||||||
|
<value>This theme suggests its own chat channel colours.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
|
||||||
|
<value>Apply</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||||
|
<value>Keep current</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -2148,6 +2148,24 @@ namespace HellionChat.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself..
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_ColorSelectedInputChannelButton_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Tint channel selector with channel colour.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_ColorSelectedInputChannelButton_Name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Chat colours.
|
/// Looks up a localized string similar to Chat colours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2660,7 +2678,25 @@ namespace HellionChat.Resources {
|
|||||||
return ResourceManager.GetString("Options_HideInBattle_Name", resourceCulture);
|
return ResourceManager.GetString("Options_HideInBattle_Name", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again..
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_HideInNewGamePlusMenu_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Hide while New Game+ menu is open.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_HideInNewGamePlusMenu_Name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Hide {0} during loading screens..
|
/// Looks up a localized string similar to Hide {0} during loading screens..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -208,6 +208,12 @@
|
|||||||
<data name="Options_ChatColours_Import">
|
<data name="Options_ChatColours_Import">
|
||||||
<value>Vom Spiel importieren</value>
|
<value>Vom Spiel importieren</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||||
|
<value>Channel-Auswahl-Knopf in Channel-Farbe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||||
|
<value>Der Channel-Auswahl-Knopf neben dem Eingabefeld bekommt die Farbe des aktuell aktiven Channels. Konsistent zur Färbung des Eingabetextes selbst.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Tabs_Tab">
|
<data name="Options_Tabs_Tab">
|
||||||
<value>Kanäle</value>
|
<value>Kanäle</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1190,6 +1196,12 @@ Sie wurden gewarnt.</value>
|
|||||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||||
<value>Blende den Chat während der Kämpfe aus.</value>
|
<value>Blende den Chat während der Kämpfe aus.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||||
|
<value>Während des New-Game+ Menüs ausblenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||||
|
<value>Blendet den Chat aus, solange das New-Game+ Menü geöffnet ist. Schließen des Menüs blendet den Chat wieder ein.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||||
<value>Emote-Statistik</value>
|
<value>Emote-Statistik</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -208,6 +208,12 @@
|
|||||||
<data name="Options_ChatColours_Import">
|
<data name="Options_ChatColours_Import">
|
||||||
<value>Import from game</value>
|
<value>Import from game</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||||
|
<value>Tint channel selector with channel colour</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||||
|
<value>The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Tabs_Tab">
|
<data name="Options_Tabs_Tab">
|
||||||
<value>Tabs</value>
|
<value>Tabs</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1189,6 +1195,12 @@
|
|||||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||||
<value>Hide the chat during battles.</value>
|
<value>Hide the chat during battles.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||||
|
<value>Hide while New Game+ menu is open</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||||
|
<value>Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||||
<value>Emote Stats</value>
|
<value>Emote Stats</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class Chat2Classic
|
||||||
|
{
|
||||||
|
public const string Slug = "chat2-classic";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Chat 2 Klassik",
|
||||||
|
Author: "Upstream (Infi & Anna)",
|
||||||
|
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#141414"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#202020"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
||||||
|
Border: ColourUtil.HexToRgba("#404040"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#999999"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#666666"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f,
|
||||||
|
FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f,
|
||||||
|
ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class EventHorizon
|
||||||
|
{
|
||||||
|
public const string Slug = "event-horizon";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Event Horizon",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#040308"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1B1530"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
||||||
|
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||||
|
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||||
|
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
||||||
|
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
||||||
|
// Lila. Channel-Identität bleibt klar erkennbar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class HellionArctic
|
||||||
|
{
|
||||||
|
public const string Slug = "hellion-arctic";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Hellion Arctic",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#0097A7"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#00BED2"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#00BED299"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#E85D04"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#F97316"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#FB923C"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#0097A7"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#070B12"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#0C1220"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#141E30"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1A2538"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#22303F"),
|
||||||
|
Border: ColourUtil.HexToRgba("#00BED266"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6F4F1"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#8FA3B5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#566273"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#00BED2")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f,
|
||||||
|
FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f,
|
||||||
|
ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
|
||||||
|
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class MintGrove
|
||||||
|
{
|
||||||
|
public const string Slug = "mint-grove";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Mint Grove",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#3CB371"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#F4C870"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#FCDD93"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0A1410"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#10201A"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#162B22"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1E372B"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#284335"),
|
||||||
|
Border: ColourUtil.HexToRgba("#5DD39E55"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E8F5EA"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9BB5A5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#5C6F65"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#5DA9C7")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f,
|
||||||
|
FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f,
|
||||||
|
ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
|
||||||
|
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
|
||||||
|
// bleibt erhalten für Erkennbarkeit.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class MoonlitBloom
|
||||||
|
{
|
||||||
|
public const string Slug = "moonlit-bloom";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Moonlit Bloom",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||||
|
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||||
|
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||||
|
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||||
|
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slug": "example-custom",
|
||||||
|
"name": "Example Custom",
|
||||||
|
"author": "You",
|
||||||
|
"description": "Starting template — duplicate, rename, edit colors and reload.",
|
||||||
|
"colors": {
|
||||||
|
"primaryDark": "#0097A7",
|
||||||
|
"primary": "#00BED2",
|
||||||
|
"primaryLight": "#4DD9E8",
|
||||||
|
"primaryGlow": "#00BED299",
|
||||||
|
"accentDark": "#E85D04",
|
||||||
|
"accent": "#F97316",
|
||||||
|
"accentLight": "#FB923C",
|
||||||
|
"identity": "#0097A7",
|
||||||
|
"windowBg": "#070B12",
|
||||||
|
"childBg": "#0C1220",
|
||||||
|
"frameBg": "#141E30",
|
||||||
|
"surface": "#1A2538",
|
||||||
|
"surfaceHover": "#22303F",
|
||||||
|
"border": "#00BED266",
|
||||||
|
"textPrimary": "#E6F4F1",
|
||||||
|
"textMuted": "#8FA3B5",
|
||||||
|
"textDim": "#566273",
|
||||||
|
"statusSuccess": "#5CB85C",
|
||||||
|
"statusDanger": "#D9534F",
|
||||||
|
"statusWarning": "#F0AD4E",
|
||||||
|
"statusInfo": "#00BED2"
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"windowRounding": 4,
|
||||||
|
"childRounding": 3,
|
||||||
|
"popupRounding": 3,
|
||||||
|
"frameRounding": 2,
|
||||||
|
"grabRounding": 2,
|
||||||
|
"tabRounding": 2,
|
||||||
|
"scrollbarRounding": 2,
|
||||||
|
"windowBorderSize": 1,
|
||||||
|
"frameBorderSize": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
public sealed record Theme(
|
||||||
|
string Slug,
|
||||||
|
string Name,
|
||||||
|
string Author,
|
||||||
|
string Description,
|
||||||
|
ThemeColors Colors,
|
||||||
|
ThemeLayout Layout,
|
||||||
|
ThemeTypography Typography,
|
||||||
|
bool IsBuiltIn,
|
||||||
|
ThemeChatColors? ChatColors = null
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using HellionChat.Code;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der
|
||||||
|
// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden.
|
||||||
|
// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel-
|
||||||
|
// Farben unverändert.
|
||||||
|
public sealed record ThemeChatColors(
|
||||||
|
IReadOnlyDictionary<ChatType, uint> Channels
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui.
|
||||||
|
public sealed record ThemeColors(
|
||||||
|
uint PrimaryDark,
|
||||||
|
uint Primary,
|
||||||
|
uint PrimaryLight,
|
||||||
|
uint PrimaryGlow,
|
||||||
|
|
||||||
|
uint AccentDark,
|
||||||
|
uint Accent,
|
||||||
|
uint AccentLight,
|
||||||
|
|
||||||
|
uint Identity,
|
||||||
|
|
||||||
|
uint WindowBg,
|
||||||
|
uint ChildBg,
|
||||||
|
uint FrameBg,
|
||||||
|
uint Surface,
|
||||||
|
uint SurfaceHover,
|
||||||
|
uint Border,
|
||||||
|
|
||||||
|
uint TextPrimary,
|
||||||
|
uint TextMuted,
|
||||||
|
uint TextDim,
|
||||||
|
|
||||||
|
uint StatusSuccess,
|
||||||
|
uint StatusDanger,
|
||||||
|
uint StatusWarning,
|
||||||
|
uint StatusInfo
|
||||||
|
);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
internal static class ThemeJsonLoader
|
||||||
|
{
|
||||||
|
public const int SupportedSchemaVersion = 1;
|
||||||
|
|
||||||
|
public static Theme LoadFromString(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
throw new FormatException("Theme JSON is empty");
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
try { doc = JsonDocument.Parse(json); }
|
||||||
|
catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); }
|
||||||
|
|
||||||
|
using (doc)
|
||||||
|
{
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var schemaVersion = ReadInt(root, "schemaVersion");
|
||||||
|
if (schemaVersion != SupportedSchemaVersion)
|
||||||
|
throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}");
|
||||||
|
|
||||||
|
var slug = ReadString(root, "slug");
|
||||||
|
var name = ReadString(root, "name");
|
||||||
|
var author = ReadString(root, "author");
|
||||||
|
var description = ReadString(root, "description");
|
||||||
|
|
||||||
|
var colors = ReadColors(root.GetProperty("colors"));
|
||||||
|
var layout = ReadLayout(root.GetProperty("layout"));
|
||||||
|
|
||||||
|
ThemeChatColors? chatColors = null;
|
||||||
|
if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object)
|
||||||
|
chatColors = ReadChatColors(ch);
|
||||||
|
|
||||||
|
return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThemeChatColors ReadChatColors(JsonElement el)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<HellionChat.Code.ChatType, uint>();
|
||||||
|
foreach (var prop in el.EnumerateObject())
|
||||||
|
{
|
||||||
|
// Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"),
|
||||||
|
// Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names
|
||||||
|
// werden still übersprungen — Forward-Compat falls SE neue Channels
|
||||||
|
// einführt.
|
||||||
|
if (!Enum.TryParse<HellionChat.Code.ChatType>(prop.Name, ignoreCase: true, out var channel))
|
||||||
|
continue;
|
||||||
|
if (prop.Value.ValueKind != JsonValueKind.String)
|
||||||
|
continue;
|
||||||
|
var hex = prop.Value.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(hex))
|
||||||
|
continue;
|
||||||
|
dict[channel] = HellionChat.Util.ColourUtil.HexToRgba(hex);
|
||||||
|
}
|
||||||
|
return new ThemeChatColors(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Theme LoadFromFile(string path)
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return LoadFromString(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThemeColors ReadColors(JsonElement el) => new(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
|
||||||
|
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
|
||||||
|
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
|
||||||
|
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
|
||||||
|
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
|
||||||
|
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
|
||||||
|
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
|
||||||
|
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
|
||||||
|
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
|
||||||
|
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
|
||||||
|
);
|
||||||
|
|
||||||
|
private static ThemeLayout ReadLayout(JsonElement el) => new(
|
||||||
|
WindowRounding: ReadFloat(el, "windowRounding"),
|
||||||
|
ChildRounding: ReadFloat(el, "childRounding"),
|
||||||
|
PopupRounding: ReadFloat(el, "popupRounding"),
|
||||||
|
FrameRounding: ReadFloat(el, "frameRounding"),
|
||||||
|
GrabRounding: ReadFloat(el, "grabRounding"),
|
||||||
|
TabRounding: ReadFloat(el, "tabRounding"),
|
||||||
|
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
|
||||||
|
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
|
||||||
|
FrameBorderSize: ReadFloat(el, "frameBorderSize")
|
||||||
|
);
|
||||||
|
|
||||||
|
private static string ReadString(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String)
|
||||||
|
throw new FormatException($"Theme JSON missing string property '{name}'");
|
||||||
|
return v.GetString() ?? throw new FormatException($"Theme JSON property '{name}' is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
|
||||||
|
throw new FormatException($"Theme JSON missing number property '{name}'");
|
||||||
|
return v.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ReadFloat(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
|
||||||
|
throw new FormatException($"Theme JSON missing number property '{name}'");
|
||||||
|
return (float)v.GetDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
internal static class ThemeJsonWriter
|
||||||
|
{
|
||||||
|
public static string Serialize(Theme theme)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true }))
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteNumber("schemaVersion", ThemeJsonLoader.SupportedSchemaVersion);
|
||||||
|
writer.WriteString("slug", theme.Slug);
|
||||||
|
writer.WriteString("name", theme.Name);
|
||||||
|
writer.WriteString("author", theme.Author);
|
||||||
|
writer.WriteString("description", theme.Description);
|
||||||
|
|
||||||
|
writer.WriteStartObject("colors");
|
||||||
|
WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark);
|
||||||
|
WriteColor(writer, "primary", theme.Colors.Primary);
|
||||||
|
WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight);
|
||||||
|
WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow);
|
||||||
|
WriteColor(writer, "accentDark", theme.Colors.AccentDark);
|
||||||
|
WriteColor(writer, "accent", theme.Colors.Accent);
|
||||||
|
WriteColor(writer, "accentLight", theme.Colors.AccentLight);
|
||||||
|
WriteColor(writer, "identity", theme.Colors.Identity);
|
||||||
|
WriteColor(writer, "windowBg", theme.Colors.WindowBg);
|
||||||
|
WriteColor(writer, "childBg", theme.Colors.ChildBg);
|
||||||
|
WriteColor(writer, "frameBg", theme.Colors.FrameBg);
|
||||||
|
WriteColor(writer, "surface", theme.Colors.Surface);
|
||||||
|
WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover);
|
||||||
|
WriteColor(writer, "border", theme.Colors.Border);
|
||||||
|
WriteColor(writer, "textPrimary", theme.Colors.TextPrimary);
|
||||||
|
WriteColor(writer, "textMuted", theme.Colors.TextMuted);
|
||||||
|
WriteColor(writer, "textDim", theme.Colors.TextDim);
|
||||||
|
WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess);
|
||||||
|
WriteColor(writer, "statusDanger", theme.Colors.StatusDanger);
|
||||||
|
WriteColor(writer, "statusWarning", theme.Colors.StatusWarning);
|
||||||
|
WriteColor(writer, "statusInfo", theme.Colors.StatusInfo);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
|
||||||
|
writer.WriteStartObject("layout");
|
||||||
|
writer.WriteNumber("windowRounding", theme.Layout.WindowRounding);
|
||||||
|
writer.WriteNumber("childRounding", theme.Layout.ChildRounding);
|
||||||
|
writer.WriteNumber("popupRounding", theme.Layout.PopupRounding);
|
||||||
|
writer.WriteNumber("frameRounding", theme.Layout.FrameRounding);
|
||||||
|
writer.WriteNumber("grabRounding", theme.Layout.GrabRounding);
|
||||||
|
writer.WriteNumber("tabRounding", theme.Layout.TabRounding);
|
||||||
|
writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding);
|
||||||
|
writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize);
|
||||||
|
writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
|
||||||
|
if (theme.ChatColors is { Channels.Count: > 0 } cc)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject("chatChannels");
|
||||||
|
foreach (var kvp in cc.Channels)
|
||||||
|
writer.WriteString(kvp.Key.ToString(), $"#{kvp.Value:X8}");
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteColor(Utf8JsonWriter writer, string key, uint rgba)
|
||||||
|
{
|
||||||
|
writer.WriteString(key, $"#{rgba:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht.
|
||||||
|
public sealed record ThemeLayout(
|
||||||
|
float WindowRounding,
|
||||||
|
float ChildRounding,
|
||||||
|
float PopupRounding,
|
||||||
|
float FrameRounding,
|
||||||
|
float GrabRounding,
|
||||||
|
float TabRounding,
|
||||||
|
float ScrollbarRounding,
|
||||||
|
float WindowBorderSize,
|
||||||
|
float FrameBorderSize
|
||||||
|
);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
public sealed class ThemeRegistry
|
||||||
|
{
|
||||||
|
public const string DefaultSlug = HellionArctic.Slug;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Theme> _builtIns;
|
||||||
|
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly string? _customThemesDir;
|
||||||
|
private Theme _active;
|
||||||
|
|
||||||
|
public ThemeRegistry(string? customThemesDir = null)
|
||||||
|
{
|
||||||
|
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||||
|
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||||
|
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||||
|
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||||
|
{ MintGrove.Slug, MintGrove.Build() },
|
||||||
|
};
|
||||||
|
_active = _builtIns[DefaultSlug];
|
||||||
|
_customThemesDir = customThemesDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Theme Active => _active;
|
||||||
|
|
||||||
|
public Theme Get(string slug)
|
||||||
|
{
|
||||||
|
if (_builtIns.TryGetValue(slug, out var b)) return b;
|
||||||
|
|
||||||
|
var custom = LoadCustomBySlug(slug);
|
||||||
|
if (custom != null) return custom;
|
||||||
|
|
||||||
|
return _builtIns[DefaultSlug];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Theme> AllBuiltIns() => _builtIns.Values;
|
||||||
|
|
||||||
|
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
|
||||||
|
|
||||||
|
public void Switch(string slug) => _active = Get(slug);
|
||||||
|
|
||||||
|
// Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
|
||||||
|
// LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
|
||||||
|
// neu eingelesen.
|
||||||
|
private Theme? LoadCustomBySlug(string slug)
|
||||||
|
{
|
||||||
|
if (_customThemesDir is null) return null;
|
||||||
|
if (!Directory.Exists(_customThemesDir)) return null;
|
||||||
|
|
||||||
|
foreach (var theme in RefreshCustomCache())
|
||||||
|
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return theme;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Theme> RefreshCustomCache()
|
||||||
|
{
|
||||||
|
if (_customThemesDir is null || !Directory.Exists(_customThemesDir))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var seenSlugs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var path in Directory.EnumerateFiles(_customThemesDir, "*.json"))
|
||||||
|
{
|
||||||
|
Theme? theme = null;
|
||||||
|
var stamp = File.GetLastWriteTimeUtc(path);
|
||||||
|
var key = path;
|
||||||
|
if (_customCache.TryGetValue(key, out var cached) && cached.Stamp == stamp)
|
||||||
|
{
|
||||||
|
theme = cached.Theme;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
theme = ThemeJsonLoader.LoadFromFile(path);
|
||||||
|
_customCache[key] = (theme, stamp);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Logging passiert in Plugin.cs durch den Aufrufer; hier still
|
||||||
|
// ignorieren, damit ein einzelnes kaputtes JSON nicht alle
|
||||||
|
// Custom-Themes blockt.
|
||||||
|
_ = ex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme is not null && seenSlugs.Add(theme.Slug))
|
||||||
|
yield return theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt
|
||||||
|
// für zukünftige Theme-Slots vorbereitet.
|
||||||
|
public sealed record ThemeTypography(
|
||||||
|
float? OverrideGlobalFontSizePt = null,
|
||||||
|
float? OverrideSymbolsFontSizePt = null
|
||||||
|
);
|
||||||
@@ -104,7 +104,7 @@ public sealed class ChatInputBar
|
|||||||
// window's logic but operates on _state.HistoryCursor and the shared
|
// window's logic but operates on _state.HistoryCursor and the shared
|
||||||
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
||||||
// 0 = oldest, Count-1 = newest.
|
// 0 = oldest, Count-1 = newest.
|
||||||
private unsafe int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
private int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||||
{
|
{
|
||||||
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
internal Plugin Plugin { get; }
|
internal Plugin Plugin { get; }
|
||||||
|
|
||||||
|
private readonly CommandWrapper _clearHellionCommand;
|
||||||
|
private readonly CommandWrapper _hellionCommand;
|
||||||
|
|
||||||
internal bool ScreenshotMode;
|
internal bool ScreenshotMode;
|
||||||
private string Salt { get; }
|
private string Salt { get; }
|
||||||
|
|
||||||
@@ -110,8 +113,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
SetUpTextCommandChannels();
|
SetUpTextCommandChannels();
|
||||||
SetUpAllCommands();
|
SetUpAllCommands();
|
||||||
|
|
||||||
Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
|
// Cache the registered wrapper instances so Dispose can detach the same
|
||||||
Plugin.Commands.Register("/hellion").Execute += ToggleChat;
|
// event objects the constructor attached to, without going through
|
||||||
|
// Register() again (which would re-create the wrapper if the command
|
||||||
|
// happened to be missing from the dictionary).
|
||||||
|
_clearHellionCommand = Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log");
|
||||||
|
_hellionCommand = Plugin.Commands.Register("/hellion");
|
||||||
|
_clearHellionCommand.Execute += ClearLog;
|
||||||
|
_hellionCommand.Execute += ToggleChat;
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
Plugin.ClientState.Logout += Logout;
|
Plugin.ClientState.Logout += Logout;
|
||||||
@@ -126,8 +135,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
||||||
Plugin.ClientState.Logout -= Logout;
|
Plugin.ClientState.Logout -= Logout;
|
||||||
Plugin.ClientState.Login -= Login;
|
Plugin.ClientState.Login -= Login;
|
||||||
Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
|
_hellionCommand.Execute -= ToggleChat;
|
||||||
Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
|
_clearHellionCommand.Execute -= ClearLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Logout(int _, int __)
|
private void Logout(int _, int __)
|
||||||
@@ -278,9 +287,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
case "hide":
|
case "hide":
|
||||||
CurrentHideState = HideState.User;
|
CurrentHideState = HideState.User;
|
||||||
|
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
||||||
break;
|
break;
|
||||||
case "show":
|
case "show":
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
||||||
break;
|
break;
|
||||||
case "toggle":
|
case "toggle":
|
||||||
CurrentHideState = CurrentHideState switch
|
CurrentHideState = CurrentHideState switch
|
||||||
@@ -290,6 +301,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
HideState.None => HideState.User,
|
HideState.None => HideState.User,
|
||||||
_ => CurrentHideState,
|
_ => CurrentHideState,
|
||||||
};
|
};
|
||||||
|
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,30 +418,48 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
||||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
|
Plugin.Log.Verbose("HideState: None → Battle");
|
||||||
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: Battle → None");
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
||||||
{
|
{
|
||||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
|
Plugin.Log.Verbose("HideState: None → Cutscene");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
||||||
|
{
|
||||||
|
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
||||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
|
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||||
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && Activate)
|
if (CurrentHideState == HideState.User && Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: User → None (activate)");
|
||||||
|
}
|
||||||
|
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn))
|
if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn))
|
||||||
{
|
{
|
||||||
@@ -464,9 +494,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||||
|
|
||||||
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
||||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
BgAlpha = Plugin.Config.WindowOpacity;
|
||||||
? Plugin.Config.HellionThemeWindowOpacity
|
|
||||||
: Plugin.Config.WindowAlpha / 100f;
|
|
||||||
|
|
||||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||||
WasDocked = ImGui.IsWindowDocked();
|
WasDocked = ImGui.IsWindowDocked();
|
||||||
@@ -498,8 +526,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Config.KeepInputFocus && Activate)
|
if (Plugin.Config.KeepInputFocus && Activate)
|
||||||
ImGui.SetWindowFocus(WindowName);
|
ImGui.SetWindowFocus(WindowName);
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
// zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw
|
||||||
|
// pusht das aktive Hellion-Theme global; ChatLogWindow zeichnet sich
|
||||||
|
// damit konsistent zu Settings/Pop-Out/Wizard. Wer den Upstream-Look
|
||||||
|
// will, wählt das Built-In-Theme "Chat 2 Klassik" in Settings → Themes.
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PostDraw()
|
public override void PostDraw()
|
||||||
@@ -510,9 +541,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
// doesn't get called if the input is disabled.
|
// doesn't get called if the input is disabled.
|
||||||
if (Plugin.CurrentTab.InputDisabled)
|
if (Plugin.CurrentTab.InputDisabled)
|
||||||
Activate = false;
|
Activate = false;
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
@@ -576,10 +604,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.InputPreview.CalculatePreview();
|
Plugin.InputPreview.CalculatePreview();
|
||||||
|
|
||||||
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
|
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
|
||||||
// sits above the tab area / sidebar in full window width. Stash the
|
// sits above the tab area / sidebar in full window width. ImGui's
|
||||||
// height for GetRemainingHeightForMessageLog so the message log
|
// GetContentRegionAvail subtracts its height automatically because the
|
||||||
// shrinks accordingly while the banner is visible.
|
// cursor advances past it before the message log calls
|
||||||
_v061HintBannerHeight = DrawV061HintBannerIfNeeded();
|
// GetRemainingHeightForMessageLog, so we don't track the height here.
|
||||||
|
DrawV061HintBannerIfNeeded();
|
||||||
|
|
||||||
if (Plugin.Config.SidebarTabView)
|
if (Plugin.Config.SidebarTabView)
|
||||||
DrawTabSidebar();
|
DrawTabSidebar();
|
||||||
@@ -600,9 +629,40 @@ public sealed class ChatLogWindow : Window
|
|||||||
DrawChannelName(activeTab);
|
DrawChannelName(activeTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.0.2 — compute inputColour up front so the channel selector button
|
||||||
|
// can also tint with it (existing input-text push remains below).
|
||||||
|
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
|
||||||
|
var isCommand = Chat.Trim().StartsWith('/');
|
||||||
|
if (isCommand)
|
||||||
|
{
|
||||||
|
var command = Chat.Split(' ')[0];
|
||||||
|
if (TextCommandChannels.TryGetValue(command, out var channel))
|
||||||
|
inputType = channel;
|
||||||
|
|
||||||
|
if (!IsValidCommand(command))
|
||||||
|
inputType = ChatType.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
|
||||||
|
|
||||||
|
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
|
||||||
|
inputColour = overrideColour;
|
||||||
|
|
||||||
|
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
|
||||||
|
inputColour = ecColour;
|
||||||
|
|
||||||
var beforeIcon = ImGui.GetCursorPos();
|
var beforeIcon = ImGui.GetCursorPos();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
|
|
||||||
ImGui.OpenPopup(ChatChannelPicker);
|
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
||||||
|
var selectorAbgr = tintSelector ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0u;
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, selectorAbgr, tintSelector))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.AdjustBrightness(selectorAbgr, 1.15f), tintSelector))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.AdjustBrightness(selectorAbgr, 0.85f), tintSelector))
|
||||||
|
{
|
||||||
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
|
||||||
|
ImGui.OpenPopup(ChatChannelPicker);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTab.Channel is not null && ImGui.IsItemHovered())
|
if (activeTab.Channel is not null && ImGui.IsItemHovered())
|
||||||
ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled);
|
ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled);
|
||||||
@@ -626,27 +686,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0);
|
var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0);
|
||||||
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight);
|
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight);
|
||||||
|
|
||||||
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
|
|
||||||
var isCommand = Chat.Trim().StartsWith('/');
|
|
||||||
if (isCommand)
|
|
||||||
{
|
|
||||||
var command = Chat.Split(' ')[0];
|
|
||||||
if (TextCommandChannels.TryGetValue(command, out var channel))
|
|
||||||
inputType = channel;
|
|
||||||
|
|
||||||
if (!IsValidCommand(command))
|
|
||||||
inputType = ChatType.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalColor = ImGui.GetColorU32(ImGuiCol.Text);
|
var normalColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
|
|
||||||
|
|
||||||
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
|
|
||||||
inputColour = overrideColour;
|
|
||||||
|
|
||||||
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
|
|
||||||
inputColour = ecColour;
|
|
||||||
|
|
||||||
var push = inputColour != null;
|
var push = inputColour != null;
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push))
|
using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push))
|
||||||
{
|
{
|
||||||
@@ -1508,11 +1548,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
var startY = ImGui.GetCursorPosY();
|
var startY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
||||||
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
||||||
|
|
||||||
var dismiss = false;
|
var dismiss = false;
|
||||||
var openSettings = false;
|
var openSettings = false;
|
||||||
|
// RAII for the style stack so an early return in this block
|
||||||
|
// (or a later refactor that introduces one) can never leave the
|
||||||
|
// ImGui style stack unbalanced. Matches the convention used
|
||||||
|
// elsewhere in this file.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, bg))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1f))
|
||||||
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
|
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
|
||||||
{
|
{
|
||||||
if (child)
|
if (child)
|
||||||
@@ -1529,8 +1572,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
if (dismiss)
|
if (dismiss)
|
||||||
@@ -1604,13 +1645,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
internal readonly List<bool> PopOutDocked = [];
|
internal readonly List<bool> PopOutDocked = [];
|
||||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||||
|
|
||||||
// Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the
|
|
||||||
// current frame, read by GetRemainingHeightForMessageLog so the message
|
|
||||||
// log can shrink. Unconditionally reassigned at the top of DrawChatLog
|
|
||||||
// (before any tab-area render) so the value is always in sync with the
|
|
||||||
// current frame. Returns 0 once the banner is dismissed.
|
|
||||||
private float _v061HintBannerHeight;
|
|
||||||
|
|
||||||
// v0.6.0 — live enumeration of all active Popout windows so the
|
// v0.6.0 — live enumeration of all active Popout windows so the
|
||||||
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
|
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
|
||||||
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
|
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
|
||||||
@@ -1713,47 +1747,55 @@ public sealed class ChatLogWindow : Window
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
|
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
|
||||||
|
try
|
||||||
clipper.Begin(AutoCompleteList.Count);
|
|
||||||
while (clipper.Step())
|
|
||||||
{
|
{
|
||||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
clipper.Begin(AutoCompleteList.Count);
|
||||||
|
while (clipper.Step())
|
||||||
{
|
{
|
||||||
var entry = AutoCompleteList[i];
|
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||||
|
|
||||||
var highlight = AutoCompleteSelection == i;
|
|
||||||
var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i;
|
|
||||||
if (i < 10)
|
|
||||||
{
|
{
|
||||||
var button = (i + 1) % 10;
|
var entry = AutoCompleteList[i];
|
||||||
var text = string.Format(Language.AutoTranslate_Completion_Key, button);
|
|
||||||
var size = ImGui.CalcTextSize(text);
|
|
||||||
|
|
||||||
ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X);
|
var highlight = AutoCompleteSelection == i;
|
||||||
|
var clicked = ImGui.Selectable($"{entry.Text}##{entry.Group}/{entry.Row}", highlight) || selected == i;
|
||||||
|
if (i < 10)
|
||||||
|
{
|
||||||
|
var button = (i + 1) % 10;
|
||||||
|
var text = string.Format(Language.AutoTranslate_Completion_Key, button);
|
||||||
|
var size = ImGui.CalcTextSize(text);
|
||||||
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
|
ImGui.SameLine(ImGui.GetContentRegionAvail().X - size.X);
|
||||||
ImGui.TextUnformatted(text);
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
|
||||||
|
ImGui.TextUnformatted(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clicked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var before = Chat[..AutoCompleteInfo.StartPos];
|
||||||
|
var after = Chat[AutoCompleteInfo.EndPos..];
|
||||||
|
var replacement = $"<at:{entry.Group},{entry.Row}>";
|
||||||
|
Chat = $"{before}{replacement}{after}";
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
Activate = true;
|
||||||
|
ActivatePos = AutoCompleteInfo.StartPos + replacement.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clicked)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var before = Chat[..AutoCompleteInfo.StartPos];
|
|
||||||
var after = Chat[AutoCompleteInfo.EndPos..];
|
|
||||||
var replacement = $"<at:{entry.Group},{entry.Row}>";
|
|
||||||
Chat = $"{before}{replacement}{after}";
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
Activate = true;
|
|
||||||
ActivatePos = AutoCompleteInfo.StartPos + replacement.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!AutoCompleteShouldScroll)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AutoCompleteShouldScroll = false;
|
||||||
|
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
|
||||||
|
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// ImGuiListClipperPtr wraps an unmanaged ImGuiListClipper allocated above.
|
||||||
|
// Without Destroy() the unmanaged block leaks per autocomplete render.
|
||||||
|
clipper.Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AutoCompleteShouldScroll)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AutoCompleteShouldScroll = false;
|
|
||||||
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
|
|
||||||
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data)
|
private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||||
|
|||||||
@@ -47,8 +47,11 @@ public class CommandHelpWindow : Window {
|
|||||||
Position = pos;
|
Position = pos;
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(width, 0),
|
// Use scaledWidth here so the size constraints stay in the same
|
||||||
MaximumSize = LogWindow.LastWindowSize with { X = width }
|
// coordinate space as Position above; otherwise the help window
|
||||||
|
// ends up the wrong width at non-100% DPI.
|
||||||
|
MinimumSize = new Vector2(scaledWidth, 0),
|
||||||
|
MaximumSize = LogWindow.LastWindowSize with { X = scaledWidth }
|
||||||
};
|
};
|
||||||
|
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using HellionChat.Themes;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ImGui style override for Hellion Chat. Industrial HUD palette with three
|
/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine
|
||||||
/// distinct accents — cyan-teal as the primary action color, industrial
|
/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window-
|
||||||
/// amber for active state highlights, slate-violet for title bars and
|
/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme
|
||||||
/// active tabs — on a deep-slate frame background with steel borders.
|
/// gelesen statt aus einer fixen Konstanten-Tabelle.
|
||||||
///
|
|
||||||
/// Two entry points:
|
|
||||||
/// Push — local color stack, scoped via using-block. Use inside
|
|
||||||
/// Hellion-only surfaces (Privacy tab, first-run wizard).
|
|
||||||
/// PushGlobal — full color + style variable stack. Pushed once per frame
|
|
||||||
/// in Plugin.Draw so every Hellion-rendered window inherits
|
|
||||||
/// the look. Cheap to pop because ImGui keeps its own stack.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class HellionStyle
|
internal static class HellionStyle
|
||||||
{
|
{
|
||||||
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
|
|
||||||
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
|
|
||||||
// expects. Hex values are sourced from the Hellion Online Media brand
|
|
||||||
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
|
|
||||||
|
|
||||||
// Primary — Arctic Cyan, used for every interactive control (buttons,
|
|
||||||
// checks, sliders, separators when hovered). Three brand stages plus a
|
|
||||||
// hover that lifts to brand-color-light and a press that drops to
|
|
||||||
// brand-color-dark.
|
|
||||||
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
|
|
||||||
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
|
|
||||||
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
|
|
||||||
|
|
||||||
// Identity — brand-color-dark teal for window title bars and the
|
|
||||||
// active tab. Sits visibly below the primary cyan on buttons so the
|
|
||||||
// user sees "where am I" (deep teal) versus "what can I click"
|
|
||||||
// (brand cyan) without leaving the cyan family.
|
|
||||||
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
|
|
||||||
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
|
|
||||||
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
|
|
||||||
|
|
||||||
// Accent — Ember Orange for warm highlights on grips and scrollbar
|
|
||||||
// pulls. Replaces the previous industrial amber so the plugin matches
|
|
||||||
// the website's CTA palette. AccentActive is reserved for any future
|
|
||||||
// pressed-state on accent surfaces; the current slots only need
|
|
||||||
// AccentRgba and AccentHoverRgba.
|
|
||||||
private const uint AccentRgba = 0xF97316FF; // accent-color
|
|
||||||
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
|
|
||||||
|
|
||||||
// Surfaces — Hellion brand background ladder. Window darkest, frame
|
|
||||||
// hover ladder climbs into surface tones. Matches the website's
|
|
||||||
// background / background-medium / background-light / surface vars.
|
|
||||||
private const uint WindowBgRgba = 0x070B12FF; // background
|
|
||||||
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
|
|
||||||
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
|
|
||||||
private const uint FrameBgRgba = 0x141E30FF; // background-light
|
|
||||||
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
|
|
||||||
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
|
|
||||||
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
|
|
||||||
private const uint BorderRgba = 0x00BED266;
|
|
||||||
private const uint BorderShadowRgba = 0x00000000;
|
|
||||||
|
|
||||||
// Headers / collapsing-headers / tree nodes / selectables — same
|
|
||||||
// surface ladder as frames so panels feel consistent.
|
|
||||||
private const uint HeaderRgba = 0x141E30FF;
|
|
||||||
private const uint HeaderHoverRgba = 0x1A2538FF;
|
|
||||||
private const uint HeaderActiveRgba = 0x22303FFF;
|
|
||||||
|
|
||||||
// Title bars — Identity teal on active so the focused window reads
|
|
||||||
// as "yours" without using accent or primary slots.
|
|
||||||
private const uint TitleBgRgba = 0x070B12FF;
|
|
||||||
private const uint TitleBgActiveRgba = IdentityRgba;
|
|
||||||
private const uint TitleBgCollapsedRgba = 0x05080EFF;
|
|
||||||
|
|
||||||
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
|
|
||||||
// active. Unfocused-active uses the deeper Identity stage so an
|
|
||||||
// unfocused window's active tab still reads but does not pull focus.
|
|
||||||
private const uint TabRgba = 0x141E30FF;
|
|
||||||
private const uint TabHoveredRgba = IdentityHoverRgba;
|
|
||||||
private const uint TabActiveRgba = IdentityRgba;
|
|
||||||
private const uint TabUnfocusedRgba = 0x0C1220FF;
|
|
||||||
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
|
|
||||||
|
|
||||||
// Scrollbar — Ember on grab so the pull stands out without competing
|
|
||||||
// with the cyan action buttons. Idle grab is a subtle surface tone,
|
|
||||||
// hover/active climb into accent.
|
|
||||||
private const uint ScrollbarBgRgba = 0x070B12FF;
|
|
||||||
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
|
|
||||||
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
|
|
||||||
private const uint ScrollbarGrabActiveRgba = AccentRgba;
|
|
||||||
|
|
||||||
// Resize grip — same Ember treatment as the scrollbar.
|
|
||||||
private const uint ResizeGripRgba = 0x141E30FF;
|
|
||||||
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
|
|
||||||
private const uint ResizeGripActiveRgba = AccentRgba;
|
|
||||||
|
|
||||||
// Separator and check mark / slider follow the primary cyan.
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Local color stack for Hellion-only surfaces. Cheap. Use inside a
|
/// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a
|
||||||
/// `using var _ = HellionStyle.Push();` block.
|
/// `using var _ = HellionStyle.Push(theme);` block.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static IDisposable Push()
|
internal static IDisposable Push(Theme theme)
|
||||||
{
|
{
|
||||||
|
var c = theme.Colors;
|
||||||
var stack = new StackHandle();
|
var stack = new StackHandle();
|
||||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Global color and style-variable stack pushed once per frame in
|
/// Global color and style-variable stack pushed once per frame in
|
||||||
/// Plugin.Draw. Covers every ImGui surface the plugin renders so the
|
/// Plugin.Draw. Drives every Hellion-rendered window from the active
|
||||||
/// Hellion look is consistent across upstream and Hellion tabs.
|
/// theme's palette and layout values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowOpacity">Window background alpha (0.5–1.0). Lower
|
/// <param name="theme">Active theme from ThemeRegistry.</param>
|
||||||
/// values let the game shine through the plugin panes.</param>
|
/// <param name="windowOpacity">Window background alpha (0.5–1.0).</param>
|
||||||
internal static IDisposable PushGlobal(float windowOpacity = 1.0f)
|
internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f)
|
||||||
{
|
{
|
||||||
|
var c = theme.Colors;
|
||||||
|
var l = theme.Layout;
|
||||||
var stack = new StackHandle();
|
var stack = new StackHandle();
|
||||||
|
|
||||||
// Mix the configured opacity into both the outer window and the
|
|
||||||
// inner content child backgrounds — without ChildBg following the
|
|
||||||
// slider the chat log stays opaque inside even when the user
|
|
||||||
// wants to see the game behind it during combat. Form fields and
|
|
||||||
// popups (FrameBg, PopupBg) still stay opaque so input is readable.
|
|
||||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||||
var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte;
|
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||||
var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte;
|
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte;
|
||||||
|
|
||||||
// Layout — geometric edges, modest rounding, single-pixel borders.
|
// Layout
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f);
|
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f);
|
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f);
|
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f);
|
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
|
||||||
|
|
||||||
// Surfaces.
|
// Surfaces
|
||||||
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
||||||
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
||||||
stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba);
|
stack.PushColor(ImGuiCol.PopupBg, c.ChildBg);
|
||||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||||
stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba);
|
stack.PushColor(ImGuiCol.BorderShadow, 0u);
|
||||||
|
|
||||||
// Frames (input fields, combos, sliders).
|
// Frames
|
||||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||||
|
|
||||||
// Title bars — tertiary identity on active.
|
// Title bars
|
||||||
stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba);
|
stack.PushColor(ImGuiCol.TitleBg, c.WindowBg);
|
||||||
stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba);
|
stack.PushColor(ImGuiCol.TitleBgActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba);
|
stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg);
|
||||||
|
|
||||||
// Buttons — primary cyan.
|
// Buttons
|
||||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||||
|
|
||||||
// Headers / selectables — slate with subtle steps.
|
// Headers / selectables
|
||||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||||
|
|
||||||
// Tabs — tertiary identity for the active tab.
|
// Tabs
|
||||||
stack.PushColor(ImGuiCol.Tab, TabRgba);
|
stack.PushColor(ImGuiCol.Tab, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba);
|
stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.TabActive, TabActiveRgba);
|
stack.PushColor(ImGuiCol.TabActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba);
|
stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg);
|
||||||
stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba);
|
stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark);
|
||||||
|
|
||||||
// Scrollbar.
|
// Scrollbar
|
||||||
stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba);
|
stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent);
|
||||||
|
|
||||||
// Resize grip — secondary amber on active.
|
// Resize grip
|
||||||
stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba);
|
stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba);
|
stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight);
|
||||||
stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba);
|
stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent);
|
||||||
|
|
||||||
// Check mark + slider grab — primary cyan.
|
// Check mark + slider grab
|
||||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||||
|
|
||||||
// Separator — primary cyan when hovered/active so the eye
|
// Separator
|
||||||
// immediately sees that splitters are interactive.
|
stack.PushColor(ImGuiCol.Separator, c.Border);
|
||||||
stack.PushColor(ImGuiCol.Separator, BorderRgba);
|
stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SeparatorActive, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba);
|
|
||||||
|
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,10 @@ public partial class InputPreview : Window
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
NextChunkIsAutoTranslate = true;
|
NextChunkIsAutoTranslate = true;
|
||||||
var payload = (AutoTranslatePayload) chunk.Link!;
|
// Malformed chunks could carry an AutoTranslateBegin icon without the matching
|
||||||
|
// payload; bail out instead of dereferencing a null Link.
|
||||||
|
if (chunk.Link is not AutoTranslatePayload payload)
|
||||||
|
return;
|
||||||
CursorPosition += $"<at:{payload.Group},{payload.Key}>".Length;
|
CursorPosition += $"<at:{payload.Group},{payload.Key}>".Length;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ internal class Popout : Window
|
|||||||
|
|
||||||
public override void PreDraw()
|
public override void PreDraw()
|
||||||
{
|
{
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
// zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw
|
||||||
|
// pusht das aktive Hellion-Theme global; Pop-Out zeichnet sich damit
|
||||||
|
// konsistent zum Haupt-Chat-Window.
|
||||||
Flags = ImGuiWindowFlags.None;
|
Flags = ImGuiWindowFlags.None;
|
||||||
if (!Plugin.Config.ShowPopOutTitleBar)
|
if (!Plugin.Config.ShowPopOutTitleBar)
|
||||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||||
@@ -91,9 +92,7 @@ internal class Popout : Window
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
BgAlpha = Plugin.Config.WindowOpacity;
|
||||||
? Plugin.Config.HellionThemeWindowOpacity
|
|
||||||
: Plugin.Config.WindowAlpha / 100f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,9 +199,6 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
|
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
|
||||||
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
@@ -229,30 +225,48 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
||||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Battle");
|
||||||
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle → None");
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
||||||
{
|
{
|
||||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Cutscene");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
||||||
|
{
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
||||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)");
|
||||||
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)");
|
||||||
|
}
|
||||||
|
|
||||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,16 @@ public class SeStringDebugger : Window
|
|||||||
default:
|
default:
|
||||||
var payloadData = payload.Encode();
|
var payloadData = payload.Encode();
|
||||||
|
|
||||||
var initialByte = payloadData.First();
|
if (payloadData.Length == 0)
|
||||||
|
{
|
||||||
|
RenderMetadataDictionary("Empty Payload", new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "Type", payload.GetType().Name },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialByte = payloadData[0];
|
||||||
if (initialByte != 0x02)
|
if (initialByte != 0x02)
|
||||||
{
|
{
|
||||||
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
|
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
|
||||||
|
|||||||
@@ -9,13 +9,21 @@ using Dalamud.Bindings.ImGui;
|
|||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
internal enum SettingsView
|
||||||
|
{
|
||||||
|
Overview,
|
||||||
|
Detail,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||||
{
|
{
|
||||||
private readonly Plugin Plugin;
|
internal readonly Plugin Plugin;
|
||||||
|
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
private List<ISettingsTab> Tabs { get; }
|
private List<ISettingsTab> Tabs { get; }
|
||||||
private int CurrentTab;
|
private int CurrentTab;
|
||||||
|
private SettingsView View = SettingsView.Overview;
|
||||||
|
private readonly SettingsOverview Overview;
|
||||||
|
|
||||||
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||||
{
|
{
|
||||||
@@ -31,10 +39,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = new Configuration();
|
Mutable = new Configuration();
|
||||||
|
|
||||||
|
Overview = new SettingsOverview(this);
|
||||||
|
|
||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new General(Plugin, Mutable),
|
new General(Plugin, Mutable),
|
||||||
new Appearance(Plugin, Mutable),
|
new Appearance(Plugin, Mutable),
|
||||||
|
new SettingsTabs.Themes(Plugin, Mutable),
|
||||||
new SettingsTabs.Window(Plugin, Mutable),
|
new SettingsTabs.Window(Plugin, Mutable),
|
||||||
new Chat(Plugin, Mutable),
|
new Chat(Plugin, Mutable),
|
||||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||||
@@ -72,40 +83,81 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
if (ImGui.IsWindowAppearing())
|
if (ImGui.IsWindowAppearing())
|
||||||
Initialise();
|
|
||||||
|
|
||||||
using (var table = ImRaii.Table("##chat2-settings-table", 2))
|
|
||||||
{
|
{
|
||||||
if (table.Success)
|
Initialise();
|
||||||
{
|
View = SettingsView.Overview;
|
||||||
ImGui.TableSetupColumn("tab", ImGuiTableColumnFlags.WidthFixed);
|
|
||||||
ImGui.TableSetupColumn("settings", ImGuiTableColumnFlags.WidthStretch);
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
for (var i = 0; i < Tabs.Count; i++)
|
|
||||||
{
|
|
||||||
if (!ImGui.Selectable($"{Tabs[i].Name}###tab-{i}", CurrentTab == i))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
CurrentTab = i;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
|
|
||||||
|
|
||||||
using var child = ImRaii.Child("##chat2-settings", new Vector2(-1, height));
|
|
||||||
if (child.Success)
|
|
||||||
Tabs[CurrentTab].Draw(changed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
// ESC im Detail-View kehrt zur Overview zurück. Window-Focus-Check ist
|
||||||
|
// Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster
|
||||||
|
// fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe
|
||||||
|
// Util/SearchSelector.cs:37).
|
||||||
|
if (View == SettingsView.Detail
|
||||||
|
&& ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
|
||||||
|
&& ImGui.IsKeyPressed(ImGuiKey.Escape))
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (View == SettingsView.Overview)
|
||||||
|
Overview.Draw();
|
||||||
|
else
|
||||||
|
DrawDetail();
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawSaveButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OpenSection(int tabIndex)
|
||||||
|
{
|
||||||
|
CurrentTab = tabIndex;
|
||||||
|
View = SettingsView.Detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OpenOverview()
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDetail()
|
||||||
|
{
|
||||||
|
// Breadcrumb-Header — Akzent-Cyan, klickbar, führt zurück zur Overview
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, 0u))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("← Settings"))
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("·");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(Tabs[CurrentTab].Name.Split("###")[0]);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Section-Content in voller Breite. Die Tab-Liste links ist überholt:
|
||||||
|
// der User ist bereits über die Card-Übersicht navigiert, eine zweite
|
||||||
|
// Tab-Liste daneben würde nur den Vanilla-Look zurückbringen. Falls
|
||||||
|
// der User in eine andere Section will, geht er zurück zur Overview
|
||||||
|
// (Breadcrumb / ESC).
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
|
||||||
|
|
||||||
|
using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height));
|
||||||
|
if (child.Success)
|
||||||
|
Tabs[CurrentTab].Draw(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSaveButtons()
|
||||||
|
{
|
||||||
var save = ImGui.Button(Language.Settings_Save);
|
var save = ImGui.Button(Language.Settings_Save);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
internal sealed class SettingsOverview
|
||||||
|
{
|
||||||
|
private readonly SettingsWindow _window;
|
||||||
|
|
||||||
|
// Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow.
|
||||||
|
// Themes ist Card-Index 2, eingeschoben zwischen Appearance und Window.
|
||||||
|
private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs =
|
||||||
|
[
|
||||||
|
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
||||||
|
(FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"),
|
||||||
|
(FontAwesomeIcon.Swatchbook, "Settings_Card_Themes_Title", "Settings_Card_Themes_Subtext"),
|
||||||
|
(FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"),
|
||||||
|
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
||||||
|
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
||||||
|
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
||||||
|
(FontAwesomeIcon.Database, "Settings_Card_Database_Title", "Settings_Card_Database_Subtext"),
|
||||||
|
(FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public SettingsOverview(SettingsWindow window)
|
||||||
|
{
|
||||||
|
_window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
var columns = avail.X >= 700f ? 3 : 2;
|
||||||
|
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
||||||
|
var cardHeight = 96f;
|
||||||
|
|
||||||
|
for (var i = 0; i < CardDefs.Length; i++)
|
||||||
|
{
|
||||||
|
var (icon, titleKey, subtextKey) = CardDefs[i];
|
||||||
|
var title = HellionStrings.ResourceManager.GetString(titleKey) ?? titleKey;
|
||||||
|
var subtext = HellionStrings.ResourceManager.GetString(subtextKey) ?? subtextKey;
|
||||||
|
DrawCard(i, icon, title, subtext, cardWidth, cardHeight);
|
||||||
|
|
||||||
|
if ((i + 1) % columns != 0 && i != CardDefs.Length - 1)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h)
|
||||||
|
{
|
||||||
|
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
||||||
|
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
||||||
|
// einzelnen InvisibleButton/Text-Items separat und das Wrapping bricht.
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
|
var cursorBefore = ImGui.GetCursorScreenPos();
|
||||||
|
var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h));
|
||||||
|
var hovered = ImGui.IsItemHovered();
|
||||||
|
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||||
|
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||||
|
|
||||||
|
// Inhalts-Overlay: Icon + Title + Subtext direkt mit DrawList in den
|
||||||
|
// Card-Bereich zeichnen, statt Cursor-Hopping mit SetCursorScreenPos.
|
||||||
|
// DrawList-Overlays ändern den Cursor nicht, BeginGroup/EndGroup
|
||||||
|
// hält den Layout-Anker stabil für SameLine.
|
||||||
|
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||||
|
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||||
|
var subtextPos = cursorBefore + new Vector2(16f, 62f);
|
||||||
|
|
||||||
|
var titleColor = ColourUtil.RgbaToAbgr(0xE6F4F1FFu);
|
||||||
|
var subtextColor = ColourUtil.RgbaToAbgr(0x8FA3B5FFu);
|
||||||
|
|
||||||
|
// Icon via FontAwesome — temporär den Font pushen, mit DrawList zeichnen
|
||||||
|
using (_window.Plugin.FontManager.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
draw.AddText(iconPos, titleColor, icon.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
draw.AddText(titlePos, titleColor, title);
|
||||||
|
draw.AddText(subtextPos, subtextColor, subtext);
|
||||||
|
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
if (clicked)
|
||||||
|
{
|
||||||
|
_window.OpenSection(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,13 @@ 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
|
||||||
|
// 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);
|
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
||||||
|
|
||||||
@@ -81,6 +88,7 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
{
|
{
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +244,10 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
ImGui.Checkbox(Language.Options_ColorSelectedInputChannelButton_Name, ref Mutable.ColorSelectedInputChannelButton);
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_ColorSelectedInputChannelButton_Description);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
||||||
{
|
{
|
||||||
foreach (var type in types)
|
foreach (var type in types)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ internal sealed class Chat : ISettingsTab
|
|||||||
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
||||||
|
|
||||||
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
||||||
|
// Snapshot of EmoteCache.State for which we last built WordPopupOptions.
|
||||||
|
// Without this, an empty FilteredSheet (e.g., the user blocked every emote)
|
||||||
|
// would trigger a refill every frame the settings tab is open.
|
||||||
|
private EmoteCache.LoadingState? WordPopupOptionsBuiltFor;
|
||||||
|
|
||||||
internal Chat(Plugin plugin, Configuration mutable)
|
internal Chat(Plugin plugin, Configuration mutable)
|
||||||
{
|
{
|
||||||
@@ -28,6 +32,7 @@ internal sealed class Chat : ISettingsTab
|
|||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
|
||||||
WordPopupOptions = RefillSheet();
|
WordPopupOptions = RefillSheet();
|
||||||
|
WordPopupOptionsBuiltFor = EmoteCache.State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SearchSelector.SelectorPopupOptions RefillSheet()
|
private SearchSelector.SelectorPopupOptions RefillSheet()
|
||||||
@@ -160,9 +165,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
|
if (EmoteCache.State is EmoteCache.LoadingState.Done
|
||||||
|
&& WordPopupOptions.FilteredSheet.Length == 0
|
||||||
|
&& WordPopupOptionsBuiltFor != EmoteCache.LoadingState.Done)
|
||||||
{
|
{
|
||||||
WordPopupOptions = RefillSheet();
|
WordPopupOptions = RefillSheet();
|
||||||
|
WordPopupOptionsBuiltFor = EmoteCache.LoadingState.Done;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ internal sealed class Database : ISettingsTab
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Delete both legacy files in one click — the previous if/else
|
||||||
|
// left the second file behind when both happened to exist.
|
||||||
if (old.Exists)
|
if (old.Exists)
|
||||||
old.Delete();
|
old.Delete();
|
||||||
else
|
if (migratedOld.Exists)
|
||||||
migratedOld.Delete();
|
migratedOld.Delete();
|
||||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
|
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
CleanupRunning = true;
|
CleanupRunning = true;
|
||||||
var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList();
|
var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList();
|
||||||
|
|
||||||
new Thread(() =>
|
var thread = new Thread(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -625,10 +625,14 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
// Bound the wait so a hung framework tick can't deadlock
|
// Bound the wait so a hung framework tick can't deadlock
|
||||||
// the background cleanup worker. See the matching comment in
|
// the background cleanup worker. See the matching comment in
|
||||||
// the retention path above for rationale.
|
// the retention path above for rationale.
|
||||||
|
// Note: FilterAllTabs() is called synchronously instead of
|
||||||
|
// FilterAllTabsAsync() — the async variant fires-and-forgets
|
||||||
|
// a Task.Run, so the .Wait() would return before the filter
|
||||||
|
// pass actually finishes. See AUDIT-2026-05-05 [QUAL-02].
|
||||||
if (!Plugin.Framework.Run(() =>
|
if (!Plugin.Framework.Run(() =>
|
||||||
{
|
{
|
||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
Plugin.MessageManager.FilterAllTabsAsync();
|
Plugin.MessageManager.FilterAllTabs();
|
||||||
}).Wait(TimeSpan.FromSeconds(5)))
|
}).Wait(TimeSpan.FromSeconds(5)))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||||
@@ -646,6 +650,9 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
CleanupRunning = false;
|
CleanupRunning = false;
|
||||||
CleanupCounts = null;
|
CleanupCounts = null;
|
||||||
}
|
}
|
||||||
}).Start();
|
});
|
||||||
|
// Background thread so a still-running cleanup doesn't hold up FFXIV exit.
|
||||||
|
thread.IsBackground = true;
|
||||||
|
thread.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
|
internal static class ThemeMockup
|
||||||
|
{
|
||||||
|
// Zeichnet ein Mini-Chat-Window-Mockup mit den Theme-Werten direkt
|
||||||
|
// ins WindowDrawList. Keine Texture, keine Allocation pro Frame —
|
||||||
|
// alles via DrawList.AddRectFilled / AddText.
|
||||||
|
public static void Draw(Vector2 origin, Vector2 size, Theme theme)
|
||||||
|
{
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
var c = theme.Colors;
|
||||||
|
|
||||||
|
// Window-Bg
|
||||||
|
draw.AddRectFilled(origin, origin + size, ColourUtil.RgbaToAbgr(c.WindowBg | 0xFFu), theme.Layout.WindowRounding);
|
||||||
|
|
||||||
|
// Title-Bar
|
||||||
|
var titleHeight = 14f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
origin,
|
||||||
|
new Vector2(origin.X + size.X, origin.Y + titleHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Identity), theme.Layout.WindowRounding);
|
||||||
|
|
||||||
|
// Tab-Bar — 3 Mini-Tabs
|
||||||
|
var tabY = origin.Y + titleHeight + 4f;
|
||||||
|
var tabHeight = 12f;
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var tabX = origin.X + 6f + i * 28f;
|
||||||
|
var color = i == 0 ? c.FrameBg : c.ChildBg;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(tabX, tabY),
|
||||||
|
new Vector2(tabX + 26f, tabY + tabHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(color), theme.Layout.TabRounding);
|
||||||
|
|
||||||
|
if (i == 0) // Active-Pill
|
||||||
|
{
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(tabX, tabY + tabHeight - 2f),
|
||||||
|
new Vector2(tabX + 26f, tabY + tabHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card-Row mit Mock-Sender + Text
|
||||||
|
var rowY = tabY + tabHeight + 6f;
|
||||||
|
var rowHeight = 18f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(origin.X + 6f, rowY),
|
||||||
|
new Vector2(origin.X + size.X - 6f, rowY + rowHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Surface), 2f);
|
||||||
|
|
||||||
|
// Akzent-Button rechts unten
|
||||||
|
var btnW = 28f;
|
||||||
|
var btnH = 10f;
|
||||||
|
var btnX = origin.X + size.X - btnW - 6f;
|
||||||
|
var btnY = origin.Y + size.Y - btnH - 6f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(btnX, btnY),
|
||||||
|
new Vector2(btnX + btnW, btnY + btnH),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent), theme.Layout.FrameRounding);
|
||||||
|
|
||||||
|
// Border um das gesamte Mockup
|
||||||
|
draw.AddRect(origin, origin + size, ColourUtil.RgbaToAbgr(c.Border), theme.Layout.WindowRounding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
|
internal sealed class Themes : ISettingsTab
|
||||||
|
{
|
||||||
|
private readonly Plugin Plugin;
|
||||||
|
private readonly Configuration Mutable;
|
||||||
|
|
||||||
|
// Tracks ob der User die Apply-Frage für das aktive Theme bereits
|
||||||
|
// beantwortet hat. Banner wird nur angezeigt wenn das Theme ein
|
||||||
|
// ChatColors-Set hat UND noch keine Antwort vorliegt UND die aktuellen
|
||||||
|
// Mutable.ChatColours davon abweichen.
|
||||||
|
private string? _applyDismissedFor;
|
||||||
|
|
||||||
|
public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes";
|
||||||
|
|
||||||
|
internal Themes(Plugin plugin, Configuration mutable)
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
Mutable = mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(bool changed)
|
||||||
|
{
|
||||||
|
var registry = Plugin.ThemeRegistry;
|
||||||
|
var active = registry.Get(Mutable.Theme);
|
||||||
|
|
||||||
|
var activeLabelTemplate = HellionStrings.ResourceManager.GetString("Settings_Themes_Active") ?? "Active: {0}";
|
||||||
|
ImGui.TextUnformatted(string.Format(activeLabelTemplate, active.Name));
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u))
|
||||||
|
ImGui.TextUnformatted(active.Author);
|
||||||
|
|
||||||
|
DrawChatColorsApplyBanner(active);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var builtInsLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_BuiltIns") ?? "Built-in themes";
|
||||||
|
ImGui.TextUnformatted(builtInsLabel);
|
||||||
|
ImGui.Spacing();
|
||||||
|
DrawThemeGrid(registry.AllBuiltIns(), active.Slug);
|
||||||
|
|
||||||
|
var customs = registry.AllCustom().ToList();
|
||||||
|
if (customs.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
var customLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_Custom") ?? "Custom themes";
|
||||||
|
ImGui.TextUnformatted(customLabel);
|
||||||
|
ImGui.Spacing();
|
||||||
|
DrawThemeGrid(customs, active.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var openFolderLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_OpenFolder") ?? "Open themes folder";
|
||||||
|
if (ImGui.Button(openFolderLabel))
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
Dalamud.Utility.Util.OpenLink(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active...";
|
||||||
|
if (ImGui.Button(exportLabel))
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var fileName = $"{active.Slug}.export.json";
|
||||||
|
var path = Path.Combine(dir, fileName);
|
||||||
|
var json = ThemeJsonWriter.Serialize(active);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawThemeGrid(IEnumerable<Theme> themes, string activeSlug)
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
var columns = avail.X >= 700f ? 3 : 2;
|
||||||
|
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
||||||
|
var cardHeight = 140f; // Mockup + Name + Author brauchen den Platz
|
||||||
|
|
||||||
|
var list = themes.ToList();
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight);
|
||||||
|
|
||||||
|
// SameLine zwischen den Cards einer Reihe; am Spalten-Ende kein
|
||||||
|
// SameLine, dann beginnt automatisch eine neue Zeile.
|
||||||
|
if ((i + 1) % columns != 0 && i != list.Count - 1)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawThemeCard(Theme theme, string activeSlug, float w, float h)
|
||||||
|
{
|
||||||
|
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
||||||
|
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
||||||
|
// einzelnen InvisibleButton-Items separat und das Wrapping bricht.
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
|
var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var cursorBefore = ImGui.GetCursorScreenPos();
|
||||||
|
var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h));
|
||||||
|
var hovered = ImGui.IsItemHovered();
|
||||||
|
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
var bg = ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu);
|
||||||
|
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bg, 4f);
|
||||||
|
|
||||||
|
if (isActive)
|
||||||
|
{
|
||||||
|
var border = ColourUtil.RgbaToAbgr(theme.Colors.Primary);
|
||||||
|
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 2f);
|
||||||
|
}
|
||||||
|
else if (hovered)
|
||||||
|
{
|
||||||
|
var border = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u);
|
||||||
|
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mini-Mockup oben — DrawList-Operation, kein Cursor-Hopping
|
||||||
|
var mockupOrigin = cursorBefore + new Vector2(12f, 12f);
|
||||||
|
var mockupSize = new Vector2(w - 24f, 60f);
|
||||||
|
ThemeMockup.Draw(mockupOrigin, mockupSize, theme);
|
||||||
|
|
||||||
|
// Name + Author direkt via DrawList (statt SetCursorScreenPos +
|
||||||
|
// TextUnformatted), damit der ImGui-Layout-Cursor stabil bleibt
|
||||||
|
// und die BeginGroup/EndGroup-Klammer den Card-Bereich als ein
|
||||||
|
// Layout-Item führt.
|
||||||
|
var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||||
|
var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
|
||||||
|
draw.AddText(cursorBefore + new Vector2(12f, 80f), textColor, theme.Name);
|
||||||
|
draw.AddText(cursorBefore + new Vector2(12f, 100f), mutedColor, theme.Author);
|
||||||
|
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
if (clicked)
|
||||||
|
{
|
||||||
|
Mutable.Theme = theme.Slug;
|
||||||
|
Plugin.ThemeRegistry.Switch(theme.Slug);
|
||||||
|
_applyDismissedFor = null; // Banner für neues Theme wieder zeigen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawChatColorsApplyBanner(Theme active)
|
||||||
|
{
|
||||||
|
// Klassik hat per Design keine ChatColors — kein Banner.
|
||||||
|
if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// User hat die Frage bereits für genau dieses Theme beantwortet.
|
||||||
|
if (_applyDismissedFor == active.Slug)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Wenn die aktuellen Channel-Colors bereits exakt mit dem Theme-Vorschlag
|
||||||
|
// übereinstimmen, gibt's nichts zu tun.
|
||||||
|
var alreadyMatching = themeChatColors.Channels.All(kvp =>
|
||||||
|
Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value);
|
||||||
|
if (alreadyMatching)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Dezent-Akzent-Banner mit Border in Theme-Primary
|
||||||
|
var border = ColourUtil.RgbaToAbgr(active.Colors.Primary);
|
||||||
|
var bgFill = ColourUtil.RgbaToAbgr((active.Colors.Surface & 0xFFFFFF00u) | 0xCCu);
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var width = ImGui.GetContentRegionAvail().X;
|
||||||
|
var height = 64f;
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
draw.AddRectFilled(origin, origin + new Vector2(width, height), bgFill, 4f);
|
||||||
|
draw.AddRect(origin, origin + new Vector2(width, height), border, 4f, ImDrawFlags.None, 1f);
|
||||||
|
|
||||||
|
var hint = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Hint")
|
||||||
|
?? "This theme suggests its own chat channel colours.";
|
||||||
|
var applyLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Apply")
|
||||||
|
?? "Apply";
|
||||||
|
var keepLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Keep")
|
||||||
|
?? "Keep current";
|
||||||
|
|
||||||
|
var textColor = ColourUtil.RgbaToAbgr(active.Colors.TextPrimary);
|
||||||
|
draw.AddText(origin + new Vector2(12f, 10f), textColor, hint);
|
||||||
|
|
||||||
|
// Buttons als InvisibleButton + DrawList-Overlay, damit sie konsistent
|
||||||
|
// zum Banner-Look bleiben.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, active.Colors.Primary))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, active.Colors.PrimaryLight))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, active.Colors.PrimaryDark))
|
||||||
|
{
|
||||||
|
ImGui.SetCursorScreenPos(origin + new Vector2(12f, 32f));
|
||||||
|
if (ImGui.Button(applyLabel))
|
||||||
|
{
|
||||||
|
foreach (var kvp in themeChatColors.Channels)
|
||||||
|
Mutable.ChatColours[kvp.Key] = kvp.Value;
|
||||||
|
_applyDismissedFor = active.Slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button(keepLabel))
|
||||||
|
{
|
||||||
|
_applyDismissedFor = active.Slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor unter den Banner setzen
|
||||||
|
ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f));
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@ internal sealed class Window : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
|
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
|
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
|
||||||
|
|
||||||
|
ImGui.Checkbox(Language.Options_HideInNewGamePlusMenu_Name, ref Mutable.HideInNewGamePlusMenu);
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_HideInNewGamePlusMenu_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -233,9 +232,6 @@ internal static class AutoTranslate
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
|
|
||||||
private static extern int memcmp(byte[] b1, byte[] b2, nuint count);
|
|
||||||
|
|
||||||
internal static void ReplaceWithPayload(ref byte[] bytes)
|
internal static void ReplaceWithPayload(ref byte[] bytes)
|
||||||
{
|
{
|
||||||
var search = "<at:"u8.ToArray();
|
var search = "<at:"u8.ToArray();
|
||||||
@@ -279,7 +275,10 @@ internal static class AutoTranslate
|
|||||||
start = -1;
|
start = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i + search.Length < bytes.Length && memcmp(bytes[i..], search, (nuint) search.Length) == 0)
|
// Pure managed comparison via Span avoids the msvcrt.dll P/Invoke,
|
||||||
|
// which is fragile under Wine and triggered an extra managed-to-
|
||||||
|
// unmanaged copy per check.
|
||||||
|
if (i + search.Length < bytes.Length && bytes.AsSpan(i, search.Length).SequenceEqual(search))
|
||||||
start = i;
|
start = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,34 @@ internal static class ColourUtil {
|
|||||||
|
|
||||||
internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF)
|
internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF)
|
||||||
=> alpha | (uint) (red << 24) | (uint) (green << 16) | (uint) (blue << 8);
|
=> alpha | (uint) (red << 24) | (uint) (green << 16) | (uint) (blue << 8);
|
||||||
|
|
||||||
|
internal static uint AdjustBrightness(uint abgr, float factor)
|
||||||
|
{
|
||||||
|
var a = (byte) ((abgr & 0xFF000000) >> 24);
|
||||||
|
var b = (byte) ((abgr & 0x00FF0000) >> 16);
|
||||||
|
var g = (byte) ((abgr & 0x0000FF00) >> 8);
|
||||||
|
var r = (byte) (abgr & 0x000000FF);
|
||||||
|
|
||||||
|
var nr = (byte) Math.Clamp(r * factor, 0f, 255f);
|
||||||
|
var ng = (byte) Math.Clamp(g * factor, 0f, 255f);
|
||||||
|
var nb = (byte) Math.Clamp(b * factor, 0f, 255f);
|
||||||
|
|
||||||
|
return ((uint) a << 24) | ((uint) nb << 16) | ((uint) ng << 8) | nr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint HexToRgba(string hex)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(hex);
|
||||||
|
var s = hex.StartsWith('#') ? hex[1..] : hex;
|
||||||
|
if (s.Length != 6 && s.Length != 8)
|
||||||
|
throw new FormatException($"Hex colour must be 6 or 8 hex digits, got {s.Length}: '{hex}'");
|
||||||
|
|
||||||
|
if (!uint.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||||
|
throw new FormatException($"Hex colour '{hex}' is not a valid hexadecimal value");
|
||||||
|
|
||||||
|
if (s.Length == 6)
|
||||||
|
value = (value << 8) | 0xFFu; // RRGGBB → RRGGBBFF
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class ColorPayload
|
|||||||
return payload;
|
return payload;
|
||||||
case 0xE9:
|
case 0xE9:
|
||||||
var param = stream.ReadByte();
|
var param = stream.ReadByte();
|
||||||
|
if (param == -1)
|
||||||
|
throw new ArgumentException("Encountered premature end of input (unexpected EOF).", nameof(stream));
|
||||||
var globalValue = (uint) GlobalParametersCache.GetValue(param - 2);
|
var globalValue = (uint) GlobalParametersCache.GetValue(param - 2);
|
||||||
payload.Enabled = true;
|
payload.Enabled = true;
|
||||||
payload.UnshiftedColor = globalValue;
|
payload.UnshiftedColor = globalValue;
|
||||||
|
|||||||
@@ -49,9 +49,21 @@ public readonly unsafe ref struct GfdFileView
|
|||||||
var entries = Entries;
|
var entries = Entries;
|
||||||
if (DirectLookup)
|
if (DirectLookup)
|
||||||
{
|
{
|
||||||
if (iconId <= entries.Length)
|
// Resolve redirects on the direct-lookup path too — the binary-search
|
||||||
|
// path follows them, and skipping them here was inconsistent for
|
||||||
|
// contiguous ID sets.
|
||||||
|
var visited = 0;
|
||||||
|
while (iconId <= entries.Length)
|
||||||
{
|
{
|
||||||
entry = entries[(int)(iconId - 1)];
|
entry = entries[(int)(iconId - 1)];
|
||||||
|
if (followRedirect && entry.Redirect != 0 && entry.Redirect != iconId)
|
||||||
|
{
|
||||||
|
if (++visited > entries.Length)
|
||||||
|
break; // cycle guard
|
||||||
|
iconId = entry.Redirect;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return !entry.IsEmpty;
|
return !entry.IsEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +158,17 @@ public readonly unsafe ref struct GfdFileView
|
|||||||
internal static class IconUtil
|
internal static class IconUtil
|
||||||
{
|
{
|
||||||
private static byte[]? GfdFile;
|
private static byte[]? GfdFile;
|
||||||
public static unsafe GfdFileView GfdFileView
|
public static GfdFileView GfdFileView
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
GfdFile ??= Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
|
if (GfdFile is null)
|
||||||
return new GfdFileView(new ReadOnlySpan<byte>(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length));
|
{
|
||||||
|
var file = Plugin.DataManager.GetFile("common/font/gfdata.gfd")
|
||||||
|
?? throw new FileNotFoundException("Failed to load common/font/gfdata.gfd from the game data.");
|
||||||
|
GfdFile = file.Data;
|
||||||
|
}
|
||||||
|
return new GfdFileView(GfdFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ internal class Lender<T>
|
|||||||
|
|
||||||
internal Lender(Func<T> ctor)
|
internal Lender(Func<T> ctor)
|
||||||
{
|
{
|
||||||
Ctor = ctor;
|
Ctor = ctor ?? throw new ArgumentNullException(nameof(ctor));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ResetCounter()
|
internal void ResetCounter()
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ namespace HellionChat.Util;
|
|||||||
|
|
||||||
public static class MemoryUtil
|
public static class MemoryUtil
|
||||||
{
|
{
|
||||||
|
// Diagnostic helper. Pointer dereferences here would crash on a null/garbage
|
||||||
|
// address and a huge length would log megabytes of raw bytes; both are easy
|
||||||
|
// to trigger from a debugger and pollute the log with potentially sensitive
|
||||||
|
// game-state. Validate the inputs before reading.
|
||||||
|
private const int MaxDumpLength = 4096;
|
||||||
|
|
||||||
public static unsafe void PrintMemoryArea(nint address, int length)
|
public static unsafe void PrintMemoryArea(nint address, int length)
|
||||||
{
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
throw new ArgumentException("Memory address cannot be zero.", nameof(address));
|
||||||
|
if (length <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be positive.");
|
||||||
|
if (length > MaxDumpLength)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), length, $"Length exceeds the {MaxDumpLength}-byte safety cap.");
|
||||||
|
|
||||||
var ptr = (byte*)address;
|
var ptr = (byte*)address;
|
||||||
var str = new StringBuilder("\n");
|
var str = new StringBuilder("\n");
|
||||||
for(var i = 0; i < length; i++)
|
for(var i = 0; i < length; i++)
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ internal class UriPayload(Uri uri) : Payload
|
|||||||
public static UriPayload ResolveUri(string rawUri)
|
public static UriPayload ResolveUri(string rawUri)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(rawUri);
|
ArgumentNullException.ThrowIfNull(rawUri);
|
||||||
|
if (string.IsNullOrWhiteSpace(rawUri))
|
||||||
|
throw new UriFormatException("URI cannot be empty or whitespace.");
|
||||||
|
|
||||||
// Check for an expected scheme '://', if not add 'https://'
|
// Check for an expected scheme '://', if not add 'https://'
|
||||||
if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://")))
|
if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://")))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace HellionChat.Util;
|
namespace HellionChat.Util;
|
||||||
@@ -23,6 +24,9 @@ internal static class StringUtil
|
|||||||
var bytes = Math.Abs(byteCount);
|
var bytes = Math.Abs(byteCount);
|
||||||
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
||||||
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
|
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
|
||||||
return (Math.Sign(byteCount) * num).ToString("N0") + suf[place];
|
// "0.#" keeps the rounded fractional digit (1.5 GB stays "1.5GB"); "N0"
|
||||||
|
// would truncate it back to integer. InvariantCulture pins the decimal
|
||||||
|
// separator to '.' so a German locale doesn't render "1,5GB".
|
||||||
|
return (Math.Sign(byteCount) * num).ToString("0.#", CultureInfo.InvariantCulture) + suf[place];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 51 KiB |
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"MessagePack": {
|
"MessagePack": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.1.4, )",
|
"requested": "[3.1.4, 4.0.0)",
|
||||||
"resolved": "3.1.4",
|
"resolved": "3.1.4",
|
||||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
},
|
},
|
||||||
"Pidgin": {
|
"Pidgin": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.5.1, )",
|
"requested": "[3.5.1, 4.0.0)",
|
||||||
"resolved": "3.5.1",
|
"resolved": "3.5.1",
|
||||||
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
||||||
},
|
},
|
||||||
"SixLabors.ImageSharp": {
|
"SixLabors.ImageSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.1.12, )",
|
"requested": "[3.1.12, 4.0.0)",
|
||||||
"resolved": "3.1.12",
|
"resolved": "3.1.12",
|
||||||
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ because no data ever leaves your machine on the maintainer's
|
|||||||
infrastructure. Independently of that, the plugin is built so that
|
infrastructure. Independently of that, the plugin is built so that
|
||||||
you can act on your own data the way the GDPR expects.
|
you can act on your own data the way the GDPR expects.
|
||||||
|
|
||||||
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,10 +23,9 @@ Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
|||||||
- The plugin does not phone home. There is no telemetry, no analytics,
|
- The plugin does not phone home. There is no telemetry, no analytics,
|
||||||
no crash reporter, no usage counter, no remote update check beyond
|
no crash reporter, no usage counter, no remote update check beyond
|
||||||
what Dalamud itself does.
|
what Dalamud itself does.
|
||||||
- Two outbound network calls exist by design: the BetterTTV emote
|
- One outbound network call exists by design: the BetterTTV emote
|
||||||
service (for chat emotes) and the Square Enix Lodestone font CDN
|
service (for chat emotes). It is documented in detail below and
|
||||||
(for the in-game symbol font). Both are documented in detail below
|
can be reasoned about per request.
|
||||||
and both can be reasoned about per request.
|
|
||||||
- You can export every message the plugin has stored, in Markdown,
|
- You can export every message the plugin has stored, in Markdown,
|
||||||
JSON or CSV, and you can wipe stored history per channel, per date
|
JSON or CSV, and you can wipe stored history per channel, per date
|
||||||
range, or globally.
|
range, or globally.
|
||||||
@@ -103,8 +102,17 @@ on your behalf.
|
|||||||
reaches BetterTTV (unavoidable for any HTTPS request); the request
|
reaches BetterTTV (unavoidable for any HTTPS request); the request
|
||||||
itself contains no identifying user data, no character name, no
|
itself contains no identifying user data, no character name, no
|
||||||
message text. Only the emote ID being looked up is in the URL path.
|
message text. Only the emote ID being looked up is in the URL path.
|
||||||
- **When it triggers:** Only when an incoming message contains an
|
- **When it triggers:**
|
||||||
emote token that is on the BetterTTV emote list.
|
- The emote *list* (global emotes plus the top-1500 community emotes
|
||||||
|
over fifteen API pages) is fetched from `api.betterttv.net` once
|
||||||
|
per session at plugin startup, provided the **Show emotes** option
|
||||||
|
is on. This first list-fetch happens before any chat message has
|
||||||
|
arrived; BetterTTV's edge therefore sees your IP as soon as the
|
||||||
|
plugin loads, not only after an emote is mentioned.
|
||||||
|
- The individual emote *images* on `cdn.betterttv.net` are fetched
|
||||||
|
on demand, only when an incoming chat message contains a token
|
||||||
|
matching one of the cached IDs. These are cached locally
|
||||||
|
(`emoteCache/`) and reused across sessions.
|
||||||
- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once
|
- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once
|
||||||
per machine and reused.
|
per machine and reused.
|
||||||
- **How to opt out:** Turn off the **Show emotes** option in
|
- **How to opt out:** Turn off the **Show emotes** option in
|
||||||
@@ -114,24 +122,22 @@ on your behalf.
|
|||||||
|
|
||||||
Source: `HellionChat/EmoteCache.cs`.
|
Source: `HellionChat/EmoteCache.cs`.
|
||||||
|
|
||||||
### 2. Square Enix Lodestone font (`img.finalfantasyxiv.com`)
|
### 2. Square Enix Lodestone font — removed in v1.0.4
|
||||||
|
|
||||||
- **What it does:** Downloads the `FFXIV_Lodestone_SSF.ttf` font file
|
Earlier versions of HellionChat (and upstream Chat 2) downloaded
|
||||||
from the official Square Enix Lodestone CDN once during font setup,
|
`FFXIV_Lodestone_SSF.ttf` from `img.finalfantasyxiv.com` once during
|
||||||
so the plugin can render in-game special symbols (job icons, item
|
font setup. That code path was a leftover from upstream's removed
|
||||||
glyphs, etc.) inside ImGui.
|
webinterface feature and was no longer consumed anywhere — the in-game
|
||||||
- **What is sent:** A single HTTPS GET request to the public Square
|
symbol glyphs (job icons, item glyphs, status effects) come from
|
||||||
Enix font URL. Your IP address reaches Square Enix (unavoidable);
|
Dalamud's bundled symbol-font helper, not from the downloaded TTF.
|
||||||
no character data, no plugin identifier, no message content.
|
|
||||||
- **When it triggers:** Once per font initialisation, not per session
|
|
||||||
if the file is already cached locally.
|
|
||||||
- **Cached:** Yes, by Dalamud's font subsystem.
|
|
||||||
- **How to opt out:** This call is part of the font pipeline inherited
|
|
||||||
from upstream Chat 2 and not toggleable from the settings UI today.
|
|
||||||
If a user-facing opt-out for this would be useful for you, please
|
|
||||||
open a feature-request issue.
|
|
||||||
|
|
||||||
Source: `HellionChat/FontManager.cs`.
|
The download was removed in v1.0.4. As of that version HellionChat
|
||||||
|
makes no automatic network call to Square Enix or to any
|
||||||
|
`finalfantasyxiv.com` host.
|
||||||
|
|
||||||
|
Cached `FFXIV_Lodestone_SSF.ttf` files left over from earlier versions
|
||||||
|
remain in `pluginConfigs/HellionChat/` until manually deleted; they
|
||||||
|
are no longer read.
|
||||||
|
|
||||||
### Links you click yourself (no automatic traffic)
|
### Links you click yourself (no automatic traffic)
|
||||||
|
|
||||||
@@ -209,14 +215,13 @@ retroactive cleanup to apply retroactively, by design.
|
|||||||
| Party | Why they appear | What reaches them | Their privacy policy |
|
| Party | Why they appear | What reaches them | Their privacy policy |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | <https://betterttv.com/privacy> |
|
| BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | <https://betterttv.com/privacy> |
|
||||||
| Square Enix | Lodestone font download (once) | HTTPS request for the font file; your IP | <https://www.square-enix.com/privacy> |
|
|
||||||
| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement> |
|
| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement> |
|
||||||
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
||||||
|
|
||||||
Square Enix and GitHub are unavoidable for anyone playing FFXIV
|
GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone
|
||||||
through Dalamud at all. BetterTTV is the only third party HellionChat
|
playing FFXIV through Dalamud at all. BetterTTV is the only third
|
||||||
introduces on top of the baseline that is not also part of using FFXIV
|
party HellionChat introduces on top of that baseline, and it is
|
||||||
or Dalamud, and BetterTTV is opt-out via settings.
|
opt-out via settings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -232,7 +237,7 @@ direct dependencies the plugin pulls in:
|
|||||||
- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV
|
- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV
|
||||||
emote pipeline), no network on its own.
|
emote pipeline), no network on its own.
|
||||||
|
|
||||||
The two network calls listed under "Outbound network calls" are
|
The single network call listed under "Outbound network calls" is
|
||||||
written directly in HellionChat's own source, not delegated to a
|
written directly in HellionChat's own source, not delegated to a
|
||||||
dependency.
|
dependency.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
|
|
||||||
**Version 1.0.0** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
**Version 1.0.3** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||||
|
|
||||||
Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
|
Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
|
||||||
|
|
||||||
@@ -62,6 +62,13 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf
|
|||||||
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
||||||
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
||||||
|
|
||||||
|
#### Custom Themes (v1.1.0)
|
||||||
|
|
||||||
|
HellionChat 1.1.0 bringt eine Theme-Engine mit fünf eingebauten Themes
|
||||||
|
(Hellion Arctic, Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove)
|
||||||
|
und ein JSON-basiertes Authoring-Format für eigene Themes. Schema und
|
||||||
|
Schritt-für-Schritt-Anleitung in [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md).
|
||||||
|
|
||||||
### Pop-Out Convenience (v0.6.0)
|
### Pop-Out Convenience (v0.6.0)
|
||||||
|
|
||||||
- **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort.
|
- **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort.
|
||||||
@@ -302,6 +309,7 @@ Dokumentation lebt unter [`docs/`](docs/).
|
|||||||
| [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. |
|
| [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. |
|
||||||
| [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. |
|
| [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. |
|
||||||
| [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. |
|
| [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. |
|
||||||
|
| [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md) | Theme-Engine-Authoring-Guide (EN): JSON-Schema, Color-/Layout-Slots, Channel-Identity-Regeln, Validierung. |
|
||||||
| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
||||||
| [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. |
|
| [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. |
|
||||||
| [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. |
|
| [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. |
|
||||||
|
|||||||
@@ -12,6 +12,85 @@ und verlinkt für Details auf die Release-Pages.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.1.0] — 2026-05-05 — Theme Foundation
|
||||||
|
|
||||||
|
Erster großer UI-Cycle nach v1.0.0. Theme-Engine, fünf Built-In-Themes,
|
||||||
|
Custom-Themes via JSON, Settings-Card-Grid.
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Theme-Engine** mit fünf Built-In-Themes: Hellion Arctic (Default),
|
||||||
|
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove.
|
||||||
|
- **Settings → Themes** mit Mini-Mockup-Preview pro Theme. Klick auf
|
||||||
|
eine Card switcht sofort das ganze Plugin (Chat, Settings, Pop-Out).
|
||||||
|
- **Custom-Themes via JSON** in `pluginConfigs/HellionChat/themes/`.
|
||||||
|
Beim ersten Start wird `example-theme.json` als Vorlage abgelegt.
|
||||||
|
- **Optional Theme-Chat-Channel-Colors**: Themes können eigene
|
||||||
|
Channel-Farben mitliefern. Beim Switch erscheint ein Banner mit
|
||||||
|
*Übernehmen / Behalten* — nie automatisch.
|
||||||
|
- **Settings-Card-Grid**: neue Übersicht beim Öffnen, Card-Klick führt
|
||||||
|
in die Detail-Ansicht der Section. Breadcrumb + ESC führen zurück.
|
||||||
|
- **`docs/THEME-AUTHORING.md`** als Anleitung zum Schreiben eigener
|
||||||
|
Themes, mit Hellion-Forge-Branding.
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- **Plugin-Icon** auf Hellion Forge Hammer (vorher ChatTwo-Derivat).
|
||||||
|
- **Settings-Detail-View** verwendet die volle Breite — die zweite
|
||||||
|
Tab-Liste links ist weg, weil die Card-Übersicht den Wechsel
|
||||||
|
übernimmt.
|
||||||
|
- **`HellionStyle.PushGlobal`** ist jetzt theme-driven (`PushGlobal(theme,
|
||||||
|
opacity)`) statt const-palette-driven.
|
||||||
|
- **Configuration v13 → v14**: alle User landen auf `hellion-arctic`.
|
||||||
|
Wer den Upstream-Look will, wählt `chat2-classic` in Settings →
|
||||||
|
Themes.
|
||||||
|
|
||||||
|
### Veraltet
|
||||||
|
|
||||||
|
- `Configuration.HellionThemeEnabled` und `HellionThemeWindowOpacity`
|
||||||
|
bleiben für ein Release lesbar als Safety-Net, werden aber nicht
|
||||||
|
mehr ausgewertet. Entfernung geplant in v1.2.0.
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
|
||||||
|
- Custom-Theme-JSON-Loader prüft `schemaVersion`, Pflichtfelder und
|
||||||
|
Hex-Format. Ungültige Themes werden mit Warning übersprungen, das
|
||||||
|
Plugin lädt mit Built-Ins weiter.
|
||||||
|
|
||||||
|
### Intern
|
||||||
|
|
||||||
|
- 51 lokale Unit-Tests (Theme-Records, Registry, JSON-Round-Trip,
|
||||||
|
Sanity pro Built-In-Theme). Tests sind gitignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.3] — 2026-05-04 — Polish patch
|
||||||
|
|
||||||
|
Vier kleine Polish-Items aus dem Backlog gebündelt:
|
||||||
|
|
||||||
|
- **Hide bei New Game+ Menü**: Optionaler globaler Toggle der Hellion
|
||||||
|
Chat (und alle weiteren Plugin-Fenster wie Settings, DB-Viewer,
|
||||||
|
Pop-Outs) ausblendet, solange das NG+-Menü offen ist. Settings →
|
||||||
|
Fenster → Rahmen, Default aus. Skipt analog zum bestehenden
|
||||||
|
LoadingScreens-Pattern den gesamten `WindowSystem.Draw()`-Pfad.
|
||||||
|
- **Channel-Selector-Färbung**: Optionales Tinting des
|
||||||
|
Channel-Auswahl-Knopfs (Comment-Icon) neben dem Eingabefeld in der
|
||||||
|
aktuellen Channel-Farbe. Settings → Aussehen → Chat-Farben, Default
|
||||||
|
an. Konsistent zur bestehenden Eingabetext-Färbung, ExtraChat-Override
|
||||||
|
wird übernommen.
|
||||||
|
- **(De)Buff-Icon Aspect-Ratio-Fix**: `PayloadHandler.InlineIcon` quetschte
|
||||||
|
alle Hover-Icons auf 32×32. Status-Icons mit nicht-quadratischen
|
||||||
|
Dimensionen (Debuffs mit Pfeil-Indikator) sind jetzt aspekt-erhaltend
|
||||||
|
geshrinkt. Eigenständige Float-Math-Implementierung mit Zero-Size-Guard
|
||||||
|
statt Cherry-Pick aus dem offenen ChatTwo PR #157 (der hatte eine
|
||||||
|
int-Division-Falle).
|
||||||
|
- **HideState-Logging-Sweep**: Alle HideState-Transitions
|
||||||
|
(Battle/Cutscene/User/Override plus die Pop-Out-Spiegelung) loggen sich
|
||||||
|
auf Verbose-Level. Aus by default, Aktivierung via
|
||||||
|
`/xllog set HellionChat verbose` für Bug-Report-Diagnose.
|
||||||
|
|
||||||
|
[Release-Notes 1.0.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3)
|
||||||
|
|
||||||
## [1.0.1] — 2026-05-04 — Window Position Recovery
|
## [1.0.1] — 2026-05-04 — Window Position Recovery
|
||||||
|
|
||||||
Fixes an off-screen-window scenario the user could end up in after a
|
Fixes an off-screen-window scenario the user could end up in after a
|
||||||
|
|||||||
@@ -12,22 +12,39 @@ Privacy-First-Schnittmenge des Plugins erweisen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nächster Cycle (v1.1.0)
|
## Nächster Cycle (v1.2.0)
|
||||||
|
|
||||||
|
**Layout Refresh** — sichtbare Modernisierung des Chat-Windows selbst.
|
||||||
|
|
||||||
|
- Top-Tabs-Refresh mit Akzent-Pill-Underline statt Background-Fill
|
||||||
|
- Sidebar-Tabs (existing) bekommen Icons + vertikale Pill am Window-Rand
|
||||||
|
- Bottom-Status-Bar (Channel-Indikator, Privacy-Badge, Tab-Count,
|
||||||
|
Tells, Version)
|
||||||
|
- Card-Rows als Default-Message-Rendering, mit Compact-Density-Toggle
|
||||||
|
- Per-Tab Custom-Icons im Tabs-Settings-Dialog
|
||||||
|
- Removal des deprecated `HellionThemeEnabled`/`HellionThemeWindowOpacity`
|
||||||
|
Configuration-Felder
|
||||||
|
|
||||||
|
Spec liegt in [[Hellion Chat UI Modernisierung Spec]] (Vault).
|
||||||
|
|
||||||
|
## v1.1.0 — Theme Foundation (released 2026-05-05)
|
||||||
|
|
||||||
|
Theme-Engine mit fünf Built-In-Themes, Settings-Card-Grid, Custom-
|
||||||
|
Themes via JSON, Theme-Authoring-Doku. Plugin-Icon auf Hellion Forge.
|
||||||
|
Siehe `docs/CHANGELOG.md` für Details.
|
||||||
|
|
||||||
|
Aus dem ursprünglichen v1.1.0-Plan (Ad-Block / Spam-Filter, Receive-
|
||||||
|
Suppressed-Tells-Toggle) wurden zugunsten der Theme-Engine zurück
|
||||||
|
gestellt — beide Items leben weiter im Mittelfrist-Block.
|
||||||
|
|
||||||
|
## Mittelfristig (v1.2.x – v1.3.0)
|
||||||
|
|
||||||
- **Ad-Block / Spam-Filter** — Hybrid-Konzept aus eigenem Light-Filter und
|
- **Ad-Block / Spam-Filter** — Hybrid-Konzept aus eigenem Light-Filter und
|
||||||
optionaler `NoSoliciting`-IPC-Integration. Adressiert Werbe-Spam in
|
optionaler `NoSoliciting`-IPC-Integration. Adressiert Werbe-Spam in
|
||||||
öffentlichen Channels und Tells. Größter Block des Cycles.
|
öffentlichen Channels und Tells. Aus dem v1.1.0-Plan zurückgestellt.
|
||||||
- **Receive-Suppressed-Tells-Toggle** — Auto-Tell-Tabs greift auch wenn ein
|
- **Receive-Suppressed-Tells-Toggle** — Auto-Tell-Tabs greift auch wenn ein
|
||||||
Drittplugin (z.B. XIVMessenger) die /tell-Anzeige global suppressed.
|
Drittplugin (z.B. XIVMessenger) die /tell-Anzeige global suppressed.
|
||||||
Gleicher Hook-Layer wie Ad-Block, deshalb gebündelt.
|
Gleicher Hook-Layer wie Ad-Block, deshalb gebündelt.
|
||||||
|
|
||||||
## Mittelfristig (v1.1.x – v1.2.0)
|
|
||||||
|
|
||||||
- **Plugin-weite Theme-Varianten** — über die ChatColours-Presets aus v0.6.0
|
|
||||||
hinaus. Mehrere komplette Window-Themes (Frame, Surface, Border, Text)
|
|
||||||
inkl. Farbfamilien mit Helligkeits-Abstufungen. Anknüpfung an
|
|
||||||
Hellion-Online-Media-Brand-Themes (Event Horizon, Night Blue, Indigo Violet
|
|
||||||
und weitere).
|
|
||||||
- **Database-Viewer Inline-Search** — Volltext-Suche im DB-Viewer via
|
- **Database-Viewer Inline-Search** — Volltext-Suche im DB-Viewer via
|
||||||
SQLite FTS5. Aktuell gibt es nur Datums- und Channel-Filter.
|
SQLite FTS5. Aktuell gibt es nur Datums- und Channel-Filter.
|
||||||
- **TempTell Persistence** — Pin-Toggle auf TempTell-Tabs damit ausgewählte
|
- **TempTell Persistence** — Pin-Toggle auf TempTell-Tabs damit ausgewählte
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Theme Authoring Guide
|
||||||
|
|
||||||
|
> Built by **Hellion Forge** — the plugin workshop arm of [Hellion Online Media](https://hellion-media.de). HellionChat ships with five built-in themes; this guide walks you through writing your own.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
1. Open Settings → Themes → **Open themes folder**
|
||||||
|
2. Copy `example-theme.json` to `<your-name>.json` in the same folder
|
||||||
|
3. Edit the file with any text editor
|
||||||
|
4. Reload the plugin (toggle off/on in `/xlplugins`)
|
||||||
|
5. Your theme appears in the Custom-Themes section in Settings → Themes
|
||||||
|
|
||||||
|
That's the whole loop. The rest of this document is reference.
|
||||||
|
|
||||||
|
## File location
|
||||||
|
|
||||||
|
```
|
||||||
|
%APPDATA%\XIVLauncher\pluginConfigs\HellionChat\themes\
|
||||||
|
```
|
||||||
|
|
||||||
|
(or the equivalent path on Linux/macOS — Settings → Themes → "Open themes folder" opens it directly).
|
||||||
|
|
||||||
|
Each `*.json` file in this folder is loaded as one theme. The `example-theme.json` that HellionChat seeds on first launch is your starting template.
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Theme JSON has four blocks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slug": "your-slug",
|
||||||
|
"name": "Your Theme Name",
|
||||||
|
"author": "You",
|
||||||
|
"description": "One-line description shown under the theme name.",
|
||||||
|
"colors": { ... 21 color slots ... },
|
||||||
|
"layout": { ... 9 layout values ... },
|
||||||
|
"chatChannels": { ... optional, channel-name → hex ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top-level fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `schemaVersion` | int | yes | Always `1` for HellionChat 1.1.0. The plugin warns and skips themes with a different number. |
|
||||||
|
| `slug` | string | yes | Lowercase, hyphenated. Must be unique across all themes (built-in slugs are reserved). |
|
||||||
|
| `name` | string | yes | Display name in the picker. |
|
||||||
|
| `author` | string | yes | Shown small under the theme name. |
|
||||||
|
| `description` | string | yes | One short sentence. |
|
||||||
|
| `colors` | object | yes | All 21 slots required (see below). |
|
||||||
|
| `layout` | object | yes | All 9 slots required (see below). |
|
||||||
|
| `chatChannels` | object | no | Optional channel-name → hex map (see below). |
|
||||||
|
|
||||||
|
### Color slots
|
||||||
|
|
||||||
|
All values are 6-digit `#RRGGBB` or 8-digit `#RRGGBBAA` hex strings. Six-digit values get an implicit `FF` alpha.
|
||||||
|
|
||||||
|
| Slot | Role |
|
||||||
|
|---|---|
|
||||||
|
| `primary` | Brand color — used on buttons, sliders, check marks, highlighted separators. |
|
||||||
|
| `primaryDark` | Pressed-button stage. |
|
||||||
|
| `primaryLight` | Hovered-button / link-text stage. |
|
||||||
|
| `primaryGlow` | Glow / subtle accent (typically primary with ~60% alpha). |
|
||||||
|
| `accent` | Counter-accent — scrollbar grab on hover/active, resize grip, optional CTA. |
|
||||||
|
| `accentDark` / `accentLight` | Dark/light siblings of accent. |
|
||||||
|
| `identity` | Title-bar active color and active-tab color. Often equals `primaryDark`. |
|
||||||
|
| `windowBg` | Outermost window background. |
|
||||||
|
| `childBg` | Inner panel / popup background. |
|
||||||
|
| `frameBg` | Input fields, sliders, combos. |
|
||||||
|
| `surface` | Card surfaces, headers, selectables. |
|
||||||
|
| `surfaceHover` | Hovered card / header step. |
|
||||||
|
| `border` | Panel borders. Typically primary with ~40% alpha for a brand-tinted edge. |
|
||||||
|
| `textPrimary` | Body text. Soft off-white reads better than pure `#FFFFFF` on dark backgrounds. |
|
||||||
|
| `textMuted` | Captions, secondary lines. |
|
||||||
|
| `textDim` | Disabled / hint text, separators. |
|
||||||
|
| `statusSuccess` | Green-ish for success notifications. |
|
||||||
|
| `statusDanger` | Red for errors. |
|
||||||
|
| `statusWarning` | Amber for warnings. |
|
||||||
|
| `statusInfo` | Cyan-ish info. Often equals primary. |
|
||||||
|
|
||||||
|
### Layout slots
|
||||||
|
|
||||||
|
All values are floats in pixels. `BorderSize` is 0 or 1 (no thicker borders look right with ImGui's edge anti-aliasing).
|
||||||
|
|
||||||
|
| Slot | Typical range | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `windowRounding` | 0–8 | 0 = sharp upstream look; 4–6 = softer "app" feel. |
|
||||||
|
| `childRounding` | 0–6 | Usually 1 less than `windowRounding`. |
|
||||||
|
| `popupRounding` | 0–6 | Same as `childRounding`. |
|
||||||
|
| `frameRounding` | 0–4 | For inputs, sliders. |
|
||||||
|
| `grabRounding` | 0–4 | Slider grab dot. |
|
||||||
|
| `tabRounding` | 0–4 | Tab corners. |
|
||||||
|
| `scrollbarRounding` | 0–4 | Scrollbar grab. |
|
||||||
|
| `windowBorderSize` | 0 or 1 | 1 reads better in dark themes. |
|
||||||
|
| `frameBorderSize` | 0 or 1 | Usually matches windowBorderSize. |
|
||||||
|
|
||||||
|
### Optional `chatChannels`
|
||||||
|
|
||||||
|
If present, your theme proposes its own chat-channel colors. Property names are `ChatType` enum values (case-insensitive). Unknown names are skipped silently — safe for forward-compat.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"chatChannels": {
|
||||||
|
"Say": "#FFFFFF",
|
||||||
|
"Yell": "#FFE066",
|
||||||
|
"Shout": "#FFA040",
|
||||||
|
"TellIncoming": "#FF99CC",
|
||||||
|
"TellOutgoing": "#FF99CC",
|
||||||
|
"Party": "#80C0E8",
|
||||||
|
"FreeCompany": "#4DD9E8",
|
||||||
|
"NoviceNetwork": "#A8E060",
|
||||||
|
"Linkshell1": "#A8E060"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user is asked **once per theme switch** whether to apply these colors — never auto-overwriting existing picks. The banner shows up only if your suggested colors differ from the user's current `Configuration.ChatColours`.
|
||||||
|
|
||||||
|
#### Channel-identity rule
|
||||||
|
|
||||||
|
**Don't break FFXIV channel identity.** Players have used these conventions for over a decade:
|
||||||
|
|
||||||
|
| Channel | Convention | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Say | white / off-white | Default-readable speech. |
|
||||||
|
| Yell | yellow | Urgent broadcast. |
|
||||||
|
| Shout | orange | Local urgent. |
|
||||||
|
| Tell | pink-magenta | Whisper, must stand out. |
|
||||||
|
| Party | light blue | Group ops. |
|
||||||
|
| FreeCompany | cyan-teal | Guild ops. |
|
||||||
|
| NoviceNetwork | lime-green | Mentor channel. |
|
||||||
|
|
||||||
|
A theme can tint these toward its brand family (e.g., a purple theme can shift Tell from `#FF99CC` to `#E090FF`), but **don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual hierarchy.
|
||||||
|
|
||||||
|
The four colored built-in themes (Hellion Arctic, Event Horizon, Moonlit Bloom, Mint Grove) all follow this rule — read their JSON for reference. Chat 2 Klassik intentionally ships without `chatChannels` so the user keeps their existing picks.
|
||||||
|
|
||||||
|
## Theme families
|
||||||
|
|
||||||
|
Naming convention `<color>-<modifier>` is recommended for theme families. The first member of a family is the lightest/brightest:
|
||||||
|
|
||||||
|
- `mint-grove` (current built-in, light mint)
|
||||||
|
- `forest-grove` (planned, dark emerald)
|
||||||
|
- `moss-grove` (planned, mid muted)
|
||||||
|
|
||||||
|
Code-wise families have no special handling — only the slug naming hints at the relationship. The picker may group families later, but that's not required.
|
||||||
|
|
||||||
|
## Validation and errors
|
||||||
|
|
||||||
|
When HellionChat loads your theme:
|
||||||
|
|
||||||
|
- **Schema mismatch** (`schemaVersion != 1`): theme is skipped, warning written to `/xllog`.
|
||||||
|
- **Missing required field** (e.g., no `slug`): theme is skipped, warning written.
|
||||||
|
- **Invalid hex** (e.g., `#GGHHII`): theme is skipped, warning written.
|
||||||
|
- **Unknown channel name** in `chatChannels`: that one channel is skipped silently, the rest of the theme loads normally.
|
||||||
|
|
||||||
|
Check `/xllog` after a plugin reload to see what loaded and what didn't.
|
||||||
|
|
||||||
|
## Testing your theme
|
||||||
|
|
||||||
|
1. Edit the JSON, save the file.
|
||||||
|
2. Reload the plugin: `/xlplugins` → toggle HellionChat off, then on.
|
||||||
|
3. Settings → Themes → click your theme card.
|
||||||
|
4. Watch every plugin window (chat, settings, pop-out) and pick something to fix.
|
||||||
|
5. Tweak. Reload. Repeat.
|
||||||
|
|
||||||
|
Tip: the **Settings → Themes** picker shows a mini-mockup per theme — your colors are visible before you switch.
|
||||||
|
|
||||||
|
## Sharing themes
|
||||||
|
|
||||||
|
Themes are JSON, so sharing is just a file. Drop it into someone's `pluginConfigs/HellionChat/themes/` folder and their plugin picks it up on next reload.
|
||||||
|
|
||||||
|
A community theme repository is on the Hellion Forge roadmap. Until then: share via Discord or any pastebin.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `docs/example-theme.json` (seeded automatically on first launch into `pluginConfigs/HellionChat/themes/`) — minimal valid theme.
|
||||||
|
- The five built-in themes live in source under `HellionChat/Themes/Builtin/`. They are a good reference for Color choices that work.
|
||||||
|
- [Hellion Online Media branding](https://hellion-media.de) — the Arctic Cyan + Ember Glow palette that drives the default Hellion Arctic theme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center"><sub>HellionChat is a privacy-focused fork of <a href="https://github.com/Infiziert90/ChatTwo">Chat 2</a>, distributed under the EUPL-1.2.<br/>Theme engine and authoring guide are part of <strong>Hellion Forge</strong>.</sub></p>
|
||||||
@@ -4,21 +4,22 @@ HellionChat ships and depends on a number of third-party components.
|
|||||||
This document lists them, their licences and which of them touch the
|
This document lists them, their licences and which of them touch the
|
||||||
network. It is the inventory referenced by `PRIVACY.md`.
|
network. It is the inventory referenced by `PRIVACY.md`.
|
||||||
|
|
||||||
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Direct NuGet dependencies
|
## Direct NuGet dependencies
|
||||||
|
|
||||||
Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.0.0 build.
|
Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.1.0 build.
|
||||||
|
|
||||||
| Package | Version | Licence | Network | Purpose |
|
| Package | Version | Licence | Network | Purpose |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. |
|
| [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. |
|
||||||
| [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. |
|
| [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. |
|
||||||
| [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. |
|
| [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. |
|
||||||
| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.3.0 | MIT | no | Parser combinator library used for chat-input parsing. |
|
| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.5.1 | MIT | no | Parser combinator library used for chat-input parsing. CIString Unicode fix relevant for non-ASCII channel/tab names. |
|
||||||
| [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. |
|
| [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. |
|
||||||
|
| [SQLitePCLRaw.lib.e_sqlite3](https://github.com/ericsink/SQLitePCL.raw) | 3.50.3 | MIT | no | Native SQLite binary, explicitly pinned to override the transitive default for CVE-2025-6965 (memory corruption from aggregate-term overflow) and CVE-2025-7709. |
|
||||||
|
|
||||||
Six Labors note: HellionChat is an EUPL-1.2-licensed open-source
|
Six Labors note: HellionChat is an EUPL-1.2-licensed open-source
|
||||||
project distributed at no cost. Use of ImageSharp 3.x under the
|
project distributed at no cost. Use of ImageSharp 3.x under the
|
||||||
@@ -63,8 +64,10 @@ traffic is initiated explicitly by HellionChat's own source files
|
|||||||
and is documented in `PRIVACY.md` under "Outbound network calls":
|
and is documented in `PRIVACY.md` under "Outbound network calls":
|
||||||
|
|
||||||
- `HellionChat/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting)
|
- `HellionChat/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting)
|
||||||
- `HellionChat/FontManager.cs` → Square Enix Lodestone font CDN (one-time
|
|
||||||
download)
|
The earlier Square Enix Lodestone font download (`FontManager.cs`)
|
||||||
|
was removed in v1.0.4 — it was a leftover from upstream's removed
|
||||||
|
webinterface feature and was no longer consumed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 37 KiB |