merge: v1.1.0 Theme Foundation
First major UI cycle after the standalone v1.0.0 cut. Theme engine with five built-in themes (Hellion Arctic, Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove), customisable JSON themes, modernised settings layout (card-grid overview + breadcrumb detail view), opt-in per-theme chat-channel colours, and the plugin icon swap to the Hellion Forge hammer. Configuration v13 → v14: all users land on Hellion Arctic. Pick Chat 2 Klassik in Settings → Themes for the upstream look. See HellionChat/HellionChat.yaml changelog and docs/CHANGELOG.md for the full release notes; docs/THEME-AUTHORING.md is the new guide for writing custom themes.
@@ -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`
|
||||
@@ -34,10 +34,23 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 13;
|
||||
private const int LatestVersion = 14;
|
||||
|
||||
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).
|
||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
||||
public bool PrivacyFilterEnabled = true;
|
||||
@@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration
|
||||
// Hellion Chat global ImGui theme — applied to every plugin window in
|
||||
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
||||
// can flip this off in the Privacy tab.
|
||||
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
|
||||
public bool HellionThemeEnabled = true;
|
||||
|
||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||||
// panes more glass-like so the game shines through. Default 0.5
|
||||
// matches the maintainer's daily-driver preference; users who want
|
||||
// a less translucent look bump it up in Aussehen → Theme.
|
||||
[Obsolete("Replaced by WindowOpacity in v14")]
|
||||
public float HellionThemeWindowOpacity = 0.5f;
|
||||
|
||||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
||||
@@ -321,10 +336,19 @@ public class Configuration : IPluginConfiguration
|
||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
|
||||
HellionThemeEnabled = other.HellionThemeEnabled;
|
||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||||
#pragma warning restore CS0612, CS0618
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
|
||||
// v1.1.0 theme engine fields
|
||||
Theme = other.Theme;
|
||||
WindowOpacity = other.WindowOpacity;
|
||||
ReduceMotion = other.ReduceMotion;
|
||||
UseCompactDensity = other.UseCompactDensity;
|
||||
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
||||
|
||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
||||
called out in the yaml changelog so users can see what it
|
||||
derives from. -->
|
||||
<Version>1.0.3</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||
@@ -75,6 +75,9 @@
|
||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
|
||||
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ description: |-
|
||||
- Independent plugin state — own config file and database directory,
|
||||
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.
|
||||
|
||||
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
|
||||
image_urls:
|
||||
- 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:
|
||||
- Social
|
||||
- UI
|
||||
@@ -49,6 +55,64 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
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
|
||||
|
||||
@@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
internal ExtraChat ExtraChat { get; }
|
||||
internal TypingIpc TypingIpc { get; }
|
||||
internal FontManager FontManager { get; }
|
||||
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||
|
||||
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
|
||||
// tabs: General catches the immediate-surroundings public chat
|
||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||
@@ -266,6 +288,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
ExtraChat = new ExtraChat();
|
||||
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?
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||
@@ -559,13 +589,10 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
// Hellion theme is pushed once per frame here so every plugin window
|
||||
// (chat log, settings, viewers, wizard, file dialog) renders with
|
||||
// the same palette. Skipping the push leaves the upstream Dalamud
|
||||
// look untouched for users who flipped the toggle off.
|
||||
using IDisposable? _style = Config.HellionThemeEnabled
|
||||
? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity)
|
||||
: null;
|
||||
// Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
|
||||
// Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
|
||||
// pro Frame aus der Registry gelesen.
|
||||
using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity);
|
||||
|
||||
ChatLogWindow.BeginFrame();
|
||||
|
||||
@@ -650,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin
|
||||
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
||||
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
||||
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_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
|
||||
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));
|
||||
|
||||
@@ -624,4 +624,85 @@
|
||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
|
||||
</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>
|
||||
|
||||
@@ -624,4 +624,85 @@
|
||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
|
||||
</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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -494,9 +494,7 @@ public sealed class ChatLogWindow : Window
|
||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||
|
||||
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
||||
? Plugin.Config.HellionThemeWindowOpacity
|
||||
: Plugin.Config.WindowAlpha / 100f;
|
||||
BgAlpha = Plugin.Config.WindowOpacity;
|
||||
|
||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||
WasDocked = ImGui.IsWindowDocked();
|
||||
@@ -523,28 +521,16 @@ public sealed class ChatLogWindow : Window
|
||||
return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout;
|
||||
}
|
||||
|
||||
// Tracks the style instance pushed in PreDraw so PostDraw can pop the same
|
||||
// one even if the user toggled OverrideStyle / ChosenStyle mid-frame.
|
||||
// Without this, a config change between PreDraw and PostDraw could either
|
||||
// leak a Push (no matching Pop) or pop nothing while we still have a frame
|
||||
// pushed onto the ImGui stack.
|
||||
private StyleModel? _pushedStyle;
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
if (Plugin.Config.KeepInputFocus && Activate)
|
||||
ImGui.SetWindowFocus(WindowName);
|
||||
|
||||
_pushedStyle = null;
|
||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
{
|
||||
var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle);
|
||||
if (style != null)
|
||||
{
|
||||
style.Push();
|
||||
_pushedStyle = style;
|
||||
}
|
||||
}
|
||||
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||
// 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()
|
||||
@@ -555,12 +541,6 @@ public sealed class ChatLogWindow : Window
|
||||
// doesn't get called if the input is disabled.
|
||||
if (Plugin.CurrentTab.InputDisabled)
|
||||
Activate = false;
|
||||
|
||||
if (_pushedStyle != null)
|
||||
{
|
||||
_pushedStyle.Pop();
|
||||
_pushedStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii;
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
/// <summary>
|
||||
/// ImGui style override for Hellion Chat. Industrial HUD palette with three
|
||||
/// distinct accents — cyan-teal as the primary action color, industrial
|
||||
/// amber for active state highlights, slate-violet for title bars and
|
||||
/// active tabs — on a deep-slate frame background with steel borders.
|
||||
///
|
||||
/// 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.
|
||||
/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine
|
||||
/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window-
|
||||
/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme
|
||||
/// gelesen statt aus einer fixen Konstanten-Tabelle.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Local color stack for Hellion-only surfaces. Cheap. Use inside a
|
||||
/// `using var _ = HellionStyle.Push();` block.
|
||||
/// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a
|
||||
/// `using var _ = HellionStyle.Push(theme);` block.
|
||||
/// </summary>
|
||||
internal static IDisposable Push()
|
||||
internal static IDisposable Push(Theme theme)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
var stack = new StackHandle();
|
||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
||||
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||
return stack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global color and style-variable stack pushed once per frame in
|
||||
/// Plugin.Draw. Covers every ImGui surface the plugin renders so the
|
||||
/// Hellion look is consistent across upstream and Hellion tabs.
|
||||
/// Plugin.Draw. Drives every Hellion-rendered window from the active
|
||||
/// theme's palette and layout values.
|
||||
/// </summary>
|
||||
/// <param name="windowOpacity">Window background alpha (0.5–1.0). Lower
|
||||
/// values let the game shine through the plugin panes.</param>
|
||||
internal static IDisposable PushGlobal(float windowOpacity = 1.0f)
|
||||
/// <param name="theme">Active theme from ThemeRegistry.</param>
|
||||
/// <param name="windowOpacity">Window background alpha (0.5–1.0).</param>
|
||||
internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f)
|
||||
{
|
||||
var c = theme.Colors;
|
||||
var l = theme.Layout;
|
||||
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 windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte;
|
||||
var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte;
|
||||
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte;
|
||||
|
||||
// Layout — geometric edges, modest rounding, single-pixel borders.
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
||||
// Layout
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
|
||||
|
||||
// Surfaces.
|
||||
// Surfaces
|
||||
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
||||
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
||||
stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba);
|
||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
||||
stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba);
|
||||
stack.PushColor(ImGuiCol.PopupBg, c.ChildBg);
|
||||
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||
stack.PushColor(ImGuiCol.BorderShadow, 0u);
|
||||
|
||||
// Frames (input fields, combos, sliders).
|
||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
||||
// Frames
|
||||
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||
|
||||
// Title bars — tertiary identity on active.
|
||||
stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba);
|
||||
stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba);
|
||||
stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba);
|
||||
// Title bars
|
||||
stack.PushColor(ImGuiCol.TitleBg, c.WindowBg);
|
||||
stack.PushColor(ImGuiCol.TitleBgActive, c.Identity);
|
||||
stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg);
|
||||
|
||||
// Buttons — primary cyan.
|
||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
||||
// Buttons
|
||||
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||
|
||||
// Headers / selectables — slate with subtle steps.
|
||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
||||
// Headers / selectables
|
||||
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||
|
||||
// Tabs — tertiary identity for the active tab.
|
||||
stack.PushColor(ImGuiCol.Tab, TabRgba);
|
||||
stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba);
|
||||
stack.PushColor(ImGuiCol.TabActive, TabActiveRgba);
|
||||
stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba);
|
||||
stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba);
|
||||
// Tabs
|
||||
stack.PushColor(ImGuiCol.Tab, c.FrameBg);
|
||||
stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight);
|
||||
stack.PushColor(ImGuiCol.TabActive, c.Identity);
|
||||
stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg);
|
||||
stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark);
|
||||
|
||||
// Scrollbar.
|
||||
stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba);
|
||||
// Scrollbar
|
||||
stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight);
|
||||
stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent);
|
||||
|
||||
// Resize grip — secondary amber on active.
|
||||
stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba);
|
||||
stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba);
|
||||
stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba);
|
||||
// Resize grip
|
||||
stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg);
|
||||
stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight);
|
||||
stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent);
|
||||
|
||||
// Check mark + slider grab — primary cyan.
|
||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
||||
// Check mark + slider grab
|
||||
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||
|
||||
// Separator — primary cyan when hovered/active so the eye
|
||||
// immediately sees that splitters are interactive.
|
||||
stack.PushColor(ImGuiCol.Separator, BorderRgba);
|
||||
stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba);
|
||||
stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba);
|
||||
// Separator
|
||||
stack.PushColor(ImGuiCol.Separator, c.Border);
|
||||
stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight);
|
||||
stack.PushColor(ImGuiCol.SeparatorActive, c.Primary);
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
@@ -65,23 +65,12 @@ internal class Popout : Window
|
||||
return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout;
|
||||
}
|
||||
|
||||
// Tracks the style instance pushed in PreDraw so PostDraw pops the same
|
||||
// one even if config changes mid-frame. See AUDIT-2026-05-05 [CR-UI-5].
|
||||
private StyleModel? _pushedStyle;
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
_pushedStyle = null;
|
||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
{
|
||||
var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle);
|
||||
if (style != null)
|
||||
{
|
||||
style.Push();
|
||||
_pushedStyle = style;
|
||||
}
|
||||
}
|
||||
|
||||
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||
// 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;
|
||||
if (!Plugin.Config.ShowPopOutTitleBar)
|
||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||
@@ -103,9 +92,7 @@ internal class Popout : Window
|
||||
}
|
||||
else
|
||||
{
|
||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
||||
? Plugin.Config.HellionThemeWindowOpacity
|
||||
: Plugin.Config.WindowAlpha / 100f;
|
||||
BgAlpha = Plugin.Config.WindowOpacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,12 +199,6 @@ internal class Popout : Window
|
||||
{
|
||||
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
|
||||
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
||||
|
||||
if (_pushedStyle != null)
|
||||
{
|
||||
_pushedStyle.Pop();
|
||||
_pushedStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
|
||||
@@ -9,13 +9,21 @@ using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
internal enum SettingsView
|
||||
{
|
||||
Overview,
|
||||
Detail,
|
||||
}
|
||||
|
||||
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
internal readonly Plugin Plugin;
|
||||
|
||||
private Configuration Mutable { get; }
|
||||
private List<ISettingsTab> Tabs { get; }
|
||||
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")
|
||||
{
|
||||
@@ -31,10 +39,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
Plugin = plugin;
|
||||
Mutable = new Configuration();
|
||||
|
||||
Overview = new SettingsOverview(this);
|
||||
|
||||
Tabs =
|
||||
[
|
||||
new General(Plugin, Mutable),
|
||||
new Appearance(Plugin, Mutable),
|
||||
new SettingsTabs.Themes(Plugin, Mutable),
|
||||
new SettingsTabs.Window(Plugin, Mutable),
|
||||
new Chat(Plugin, Mutable),
|
||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||
@@ -72,40 +83,81 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
public override void Draw()
|
||||
{
|
||||
if (ImGui.IsWindowAppearing())
|
||||
{
|
||||
Initialise();
|
||||
|
||||
using (var table = ImRaii.Table("##chat2-settings-table", 2))
|
||||
{
|
||||
if (table.Success)
|
||||
{
|
||||
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;
|
||||
View = SettingsView.Overview;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
// 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", new Vector2(-1, height));
|
||||
using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height));
|
||||
if (child.Success)
|
||||
Tabs[CurrentTab].Draw(changed);
|
||||
}
|
||||
Tabs[CurrentTab].Draw(false);
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
private void DrawSaveButtons()
|
||||
{
|
||||
var save = ImGui.Button(Language.Settings_Save);
|
||||
|
||||
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))
|
||||
{
|
||||
// v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten
|
||||
// Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten,
|
||||
// damit die Settings-Seite kompiliert; sie schreiben in die mit
|
||||
// [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net
|
||||
// bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen
|
||||
// gezielt für diesen Übergangs-Block.
|
||||
#pragma warning disable CS0612, CS0618
|
||||
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
#pragma warning restore CS0612, CS0618
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -62,4 +62,20 @@ internal static class ColourUtil {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
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 |
@@ -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
|
||||
you can act on your own data the way the GDPR expects.
|
||||
|
||||
Last reviewed: 2026-05-05 (HellionChat v1.0.3).
|
||||
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
- **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)
|
||||
|
||||
- **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/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/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/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. |
|
||||
|
||||
@@ -12,6 +12,58 @@ 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:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
Drittplugin (z.B. XIVMessenger) die /tell-Anzeige global suppressed.
|
||||
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
|
||||
SQLite FTS5. Aktuell gibt es nur Datums- und Channel-Filter.
|
||||
- **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,13 +4,13 @@ HellionChat ships and depends on a number of third-party components.
|
||||
This document lists them, their licences and which of them touch the
|
||||
network. It is the inventory referenced by `PRIVACY.md`.
|
||||
|
||||
Last reviewed: 2026-05-05 (HellionChat v1.0.3).
|
||||
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||
|
||||
---
|
||||
|
||||
## Direct NuGet dependencies
|
||||
|
||||
Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.0.3 build.
|
||||
Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.1.0 build.
|
||||
|
||||
| Package | Version | Licence | Network | Purpose |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
|
After Width: | Height: | Size: 37 KiB |