From c0b3edb20cb4758b34205bc9881fc64d6dfdfe62 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Wed, 6 May 2026 08:31:52 +0200 Subject: [PATCH 1/4] feat: add v1.2.1 i18n strings for new card layout --- .../Resources/HellionStrings.Designer.cs | 33 ++++++++ HellionChat/Resources/HellionStrings.de.resx | 75 +++++++++++++++++-- HellionChat/Resources/HellionStrings.resx | 75 +++++++++++++++++-- 3 files changed, 171 insertions(+), 12 deletions(-) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index bfbead7..803f4cb 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -323,4 +323,37 @@ internal class HellionStrings // Hellion Chat — v1.2.0 Appearance / Compact-Density toggle internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name)); internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description)); + + // Hellion Chat — v1.2.1 Settings Cleanup: new card titles + subtexts + internal static string Settings_Card_ThemeAndLayout_Title => Get(nameof(Settings_Card_ThemeAndLayout_Title)); + internal static string Settings_Card_ThemeAndLayout_Subtext => Get(nameof(Settings_Card_ThemeAndLayout_Subtext)); + internal static string Settings_Card_FontsAndColours_Title => Get(nameof(Settings_Card_FontsAndColours_Title)); + internal static string Settings_Card_FontsAndColours_Subtext => Get(nameof(Settings_Card_FontsAndColours_Subtext)); + internal static string Settings_Card_DataManagement_Title => Get(nameof(Settings_Card_DataManagement_Title)); + internal static string Settings_Card_DataManagement_Subtext => Get(nameof(Settings_Card_DataManagement_Subtext)); + + // Hellion Chat — v1.2.1 Theme & Layout tab section headings + WindowOpacity slider + internal static string Settings_ThemeAndLayout_Theme_Heading => Get(nameof(Settings_ThemeAndLayout_Theme_Heading)); + internal static string Settings_ThemeAndLayout_WindowStyle_Heading => Get(nameof(Settings_ThemeAndLayout_WindowStyle_Heading)); + internal static string Settings_ThemeAndLayout_TimestampStyle_Heading => Get(nameof(Settings_ThemeAndLayout_TimestampStyle_Heading)); + internal static string Settings_ThemeAndLayout_WindowOpacity_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Name)); + internal static string Settings_ThemeAndLayout_WindowOpacity_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Description)); + + // Hellion Chat — v1.2.1 Fonts & Colours tab section headings + internal static string Settings_FontsAndColours_Fonts_Heading => Get(nameof(Settings_FontsAndColours_Fonts_Heading)); + internal static string Settings_FontsAndColours_Colours_Heading => Get(nameof(Settings_FontsAndColours_Colours_Heading)); + + // Hellion Chat — v1.2.1 Data Management tab section headings + internal static string Settings_DataManagement_Storage_Heading => Get(nameof(Settings_DataManagement_Storage_Heading)); + internal static string Settings_DataManagement_Retention_Heading => Get(nameof(Settings_DataManagement_Retention_Heading)); + internal static string Settings_DataManagement_Cleanup_Heading => Get(nameof(Settings_DataManagement_Cleanup_Heading)); + internal static string Settings_DataManagement_Export_Heading => Get(nameof(Settings_DataManagement_Export_Heading)); + internal static string Settings_DataManagement_DbViewer_Heading => Get(nameof(Settings_DataManagement_DbViewer_Heading)); + internal static string Settings_DataManagement_Advanced_Heading => Get(nameof(Settings_DataManagement_Advanced_Heading)); + + // Hellion Chat — v1.2.1 Window-tab Behaviour heading (replaces Frame heading) + internal static string Settings_Window_Frame_Behaviour_Heading => Get(nameof(Settings_Window_Frame_Behaviour_Heading)); + + // Hellion Chat — v1.2.1 Migration v15 → v16 toast + internal static string Migration_v16_OverrideStyle_Toast => Get(nameof(Migration_v16_OverrideStyle_Toast)); } diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index eea3036..c3c47bd 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -639,7 +639,7 @@ Allgemein - Sprache und grundlegendes Verhalten + Plugin-globale Einstellungen — Sprache, Eingabe, Audio, Performance. Erscheinungsbild @@ -657,25 +657,25 @@ Fenster - Fensterposition, Rahmen, Hide-Zustände + Verhalten des Fensters — wann es da ist, ob es bewegt werden kann. Chat - Chat-Verhalten, Emotes, Auto-Tells + Wie Nachrichten angezeigt werden — Tells, Vorschau, Verhalten, Emotes. Tabs - Tab-Layout, Kanäle, eigene Tabs + Tab-Verwaltung — eigene Chat-Tabs anlegen und konfigurieren. Datenschutz - Filter, Aufbewahrung, Bereinigung, Export + Was darf gespeichert werden — Privacy-Filter pro Channel. Datenbank @@ -687,7 +687,7 @@ Information - Über, Mitwirkende, Support + Über das Plugin — Version, Mission, Lizenz, Changelog. Themes @@ -728,4 +728,67 @@ Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + + Theme & Layout + + + Wie das Fenster aussieht — Theme, Rahmen, Zeitstempel-Style. + + + Schriften & Farben + + + Lesbarkeit — Schriftart, Schriftgröße, Chat-Farben pro Channel. + + + Daten-Verwaltung + + + Was passiert mit gespeicherten Daten — Aufbewahrung, Aufräumen, Export, DB-Stats. + + + Theme + + + Fenster-Style + + + Zeitstempel-Style + + + Fenster-Transparenz + + + Wie durchsichtig der Fensterhintergrund ist. Niedrigere Werte lassen mehr vom Spiel durchscheinen. + + + Schriftarten + + + Chat-Farben + + + Speicherung + + + Aufbewahrung + + + Cleanup + + + Export + + + Datenbank-Viewer + + + Erweitert (Shift+Klick zum Öffnen) + + + Verhalten + + + Hellion Chat 1.2.1 hat das Settings-Menü neu sortiert und die alte „Stilüberschreiben"-Option entfernt (überholt durch das Theme-System aus 1.1.0). Deine restlichen Einstellungen bleiben unverändert. Die Fenster-Transparenz ist nach „Theme & Layout" migriert. Ein Backup der vorherigen Config liegt unter pluginConfigs/HellionChat.json.pre-v16-backup neben der aktiven HellionChat.json. + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 7616d54..82cdd16 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -639,7 +639,7 @@ General - Language and basic behaviour + Plugin-wide settings — language, input, audio, performance. Appearance @@ -657,25 +657,25 @@ Window - Window position, frame, hide states + Window behaviour — when it shows, whether it can move. Chat - Chat behaviour, emotes, auto-tells + How messages are displayed — tells, preview, behaviour, emotes. Tabs - Tab layout, channels, custom tabs + Tab management — create and configure your own chat tabs. Privacy - Filter, retention, cleanup, export + What's allowed to be stored — privacy filter per channel. Database @@ -687,7 +687,7 @@ Information - About, credits, support + About the plugin — version, mission, license, changelog. Themes @@ -728,4 +728,67 @@ Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + + Theme & Layout + + + How the window looks — theme, frame, timestamp style. + + + Fonts & Colours + + + Readability — font, font size, per-channel chat colours. + + + Data Management + + + What happens to stored data — retention, cleanup, export, DB stats. + + + Theme + + + Window Style + + + Timestamp Style + + + Window Transparency + + + How transparent the window background is. Lower values let the game show through more. + + + Fonts + + + Chat Colours + + + Storage + + + Retention + + + Cleanup + + + Export + + + Database Viewer + + + Advanced (Shift+Click to open) + + + Behaviour + + + Hellion Chat 1.2.1 reorganised the Settings menu and removed the legacy "Style override" option (made obsolete by the Themes system in 1.1.0). Your other settings are unchanged. Window opacity was migrated to Theme & Layout. A backup of your previous config is at pluginConfigs/HellionChat.json.pre-v16-backup next to the live HellionChat.json. + From ebc0999a8e559e5def1c2bffc7f1b15c6730fa2b Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Wed, 6 May 2026 08:43:54 +0200 Subject: [PATCH 2/4] refactor: re-sort settings cards thematically for v1.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split Appearance into ThemeAndLayout (theme + window-style + timestamps) and FontsAndColours (fonts + per-channel colours) - Merge Database into DataManagement together with Retention/Cleanup/Export from Privacy - Move HistoryPreload from Privacy to Chat → Auto-Tell-Tabs - Move KeybindMode from General/Language to General/Input - Drop OverrideStyle, ChosenStyle, WindowAlpha, ShowThemeQuickPicker - Migration v15 → v16 maps WindowAlpha → WindowOpacity if Opacity at default - Add card-subtext per overview card so users do not have to guess where a setting lives --- HellionChat/Configuration.cs | 11 +- HellionChat/Plugin.cs | 95 ++- HellionChat/Ui/Settings.cs | 6 +- HellionChat/Ui/SettingsOverview.cs | 23 +- HellionChat/Ui/SettingsTabs/Chat.cs | 15 + HellionChat/Ui/SettingsTabs/DataManagement.cs | 741 ++++++++++++++++++ HellionChat/Ui/SettingsTabs/Database.cs | 261 ------ .../{Appearance.cs => FontsAndColours.cs} | 120 +-- HellionChat/Ui/SettingsTabs/General.cs | 43 +- HellionChat/Ui/SettingsTabs/Privacy.cs | 523 ------------ HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs | 286 +++++++ HellionChat/Ui/SettingsTabs/Themes.cs | 223 ------ HellionChat/Ui/SettingsTabs/Window.cs | 13 +- 13 files changed, 1181 insertions(+), 1179 deletions(-) create mode 100644 HellionChat/Ui/SettingsTabs/DataManagement.cs delete mode 100755 HellionChat/Ui/SettingsTabs/Database.cs rename HellionChat/Ui/SettingsTabs/{Appearance.cs => FontsAndColours.cs} (64%) create mode 100644 HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs delete mode 100644 HellionChat/Ui/SettingsTabs/Themes.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 091e94f..0b098f3 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,7 +34,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 15; + private const int LatestVersion = 16; public int Version { get; set; } = LatestVersion; @@ -49,7 +49,6 @@ public class Configuration : IPluginConfiguration // 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. @@ -222,14 +221,10 @@ public class Configuration : IPluginConfiguration }; public float TooltipOffset; - public float WindowAlpha = 100f; public Dictionary ChatColours = new(); public bool ColorSelectedInputChannelButton = true; public List Tabs = []; - public bool OverrideStyle; - public string? ChosenStyle; - public ConfigKeyBind? ChatTabForward; public ConfigKeyBind? ChatTabBackward; @@ -294,7 +289,6 @@ public class Configuration : IPluginConfiguration ItalicFontV2 = other.ItalicFontV2; SymbolsFontSizeV2 = other.SymbolsFontSizeV2; TooltipOffset = other.TooltipOffset; - WindowAlpha = other.WindowAlpha; ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton; @@ -331,8 +325,6 @@ public class Configuration : IPluginConfiguration }).ToList(); Tabs.AddRange(liveTempTabs); - OverrideStyle = other.OverrideStyle; - ChosenStyle = other.ChosenStyle; ChatTabForward = other.ChatTabForward; ChatTabBackward = other.ChatTabBackward; @@ -353,7 +345,6 @@ public class Configuration : IPluginConfiguration WindowOpacity = other.WindowOpacity; ReduceMotion = other.ReduceMotion; UseCompactDensity = other.UseCompactDensity; - ShowThemeQuickPicker = other.ShowThemeQuickPicker; EnableAutoTellTabs = other.EnableAutoTellTabs; AutoTellTabsLimit = other.AutoTellTabsLimit; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 8a2f153..79a87ee 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -251,7 +251,6 @@ public sealed class Plugin : IDalamudPlugin // User die direkt v13 → v15 springen bekommen den Default 0.85. Config.ReduceMotion = false; Config.UseCompactDensity = false; - Config.ShowThemeQuickPicker = false; Config.Version = 14; SaveConfig(); Log.Information( @@ -273,6 +272,100 @@ public sealed class Plugin : IDalamudPlugin "(HellionThemeEnabled, HellionThemeWindowOpacity)"); } + // Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der + // Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen + // beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle, + // WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im + // Configuration-Schema definiert). WindowAlpha wird zuvor auf + // WindowOpacity gemappt damit User die ihn gesetzt hatten ihre + // Transparenz-Einstellung behalten. + if (Config.Version < 16) + { + var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; + var liveConfigPath = pluginConfigsDir is not null + ? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json") + : null; + + // Backup-Datei neben der live Config — Pattern aus v13 Branch. + if (pluginConfigsDir is not null && liveConfigPath is not null) + { + var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup"); + try + { + if (File.Exists(liveConfigPath)) + File.Copy(liveConfigPath, backupPath, overwrite: true); + } + catch (Exception ex) + { + Log.Warning(ex, "HellionChat: pre-v16 config backup failed"); + } + } + + // Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht + // mehr im Configuration-Schema sind (und damit aus Config nicht + // mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn + // User WindowOpacity noch nicht selbst angefasst hat (Default + // 0.85), sonst gewinnt der User-Wert. + float oldWindowAlpha = 100f; + bool oldOverrideStyle = false; + if (liveConfigPath is not null) + { + try + { + if (File.Exists(liveConfigPath)) + { + var rawJson = File.ReadAllText(liveConfigPath); + using var doc = System.Text.Json.JsonDocument.Parse(rawJson); + if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp) + && alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number) + { + oldWindowAlpha = alphaProp.GetSingle(); + } + if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp) + && ovProp.ValueKind is System.Text.Json.JsonValueKind.True) + { + oldOverrideStyle = true; + } + } + } + catch (Exception ex) + { + Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed"); + } + } + + if (oldWindowAlpha != 100f + && Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f) + { + Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f); + Log.Information( + $"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}"); + } + else if (oldWindowAlpha != 100f) + { + Log.Information( + $"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " + + $"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped."); + } + + if (oldOverrideStyle) + { + Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification + { + Title = "Hellion Chat 1.2.1", + Content = HellionStrings.Migration_v16_OverrideStyle_Toast, + Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, + InitialDuration = TimeSpan.FromSeconds(25), + }); + } + + Config.Version = 16; + SaveConfig(); + Log.Information( + "Migrated config v15 → v16: settings cleanup, " + + "OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema"); + } + // 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 diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 8bafce8..734e6a6 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -44,13 +44,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Tabs = [ new General(Plugin, Mutable), - new Appearance(Plugin, Mutable), - new SettingsTabs.Themes(Plugin, Mutable), + new ThemeAndLayout(Plugin, Mutable), + new FontsAndColours(Plugin, Mutable), new SettingsTabs.Window(Plugin, Mutable), new Chat(Plugin, Mutable), new SettingsTabs.Tabs(Plugin, Mutable), new SettingsTabs.Privacy(Plugin, Mutable), - new Database(Plugin, Mutable), + new DataManagement(Plugin, Mutable), new Information(Mutable), ]; diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index b0f8a96..af30a74 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -12,18 +12,21 @@ 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. + // v1.2.1: Cards thematisch re-sortiert. Theme & Layout vereint Theme- + // Picker + Frame-Style + Timestamps; Fonts & Colours vereint Schriften + // + Chat-Farben; Data Management vereint Storage + Retention + Cleanup + // + Export + DB-Viewer + Advanced. 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"), + (FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"), + (FontAwesomeIcon.Palette, "Settings_Card_ThemeAndLayout_Title", "Settings_Card_ThemeAndLayout_Subtext"), + (FontAwesomeIcon.Font, "Settings_Card_FontsAndColours_Title", "Settings_Card_FontsAndColours_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_DataManagement_Title", "Settings_Card_DataManagement_Subtext"), + (FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"), ]; public SettingsOverview(SettingsWindow window) diff --git a/HellionChat/Ui/SettingsTabs/Chat.cs b/HellionChat/Ui/SettingsTabs/Chat.cs index bb43339..b623544 100644 --- a/HellionChat/Ui/SettingsTabs/Chat.cs +++ b/HellionChat/Ui/SettingsTabs/Chat.cs @@ -89,6 +89,21 @@ internal sealed class Chat : ISettingsTab ImGui.Spacing(); ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var preload = Mutable.AutoTellTabsHistoryPreload; + ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt(HellionStrings.Privacy_AutoTellTabs_Preload_Name, ref preload, 0, 100)) + { + Mutable.AutoTellTabsHistoryPreload = preload; + } + ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description); + + ImGui.Spacing(); + ImGuiUtil.HelpText(HellionStrings.Privacy_AutoTellTabs_Preload_Hint); } } diff --git a/HellionChat/Ui/SettingsTabs/DataManagement.cs b/HellionChat/Ui/SettingsTabs/DataManagement.cs new file mode 100644 index 0000000..d0ae171 --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/DataManagement.cs @@ -0,0 +1,741 @@ +using System.Diagnostics; +using HellionChat.Code; +using HellionChat.Export; +using HellionChat.Privacy; +using HellionChat.Resources; +using HellionChat.Util; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Bindings.ImGui; + +namespace HellionChat.Ui.SettingsTabs; + +internal sealed class DataManagement : ISettingsTab +{ + private Plugin Plugin { get; } + private Configuration Mutable { get; } + + public string Name => HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement"; + + // Cleanup state (was in Privacy.cs) + private Dictionary? CleanupCounts; + private long CleanupKeepCount; + private long CleanupDeleteCount; + private bool CleanupRunning; + private bool CleanupPreviewStale; + private HashSet? CleanupPreviewSnapshot; + private bool RetentionRunning => Plugin.RetentionSweepRunning; + + // Export form state (was in Privacy.cs) + private int ExportRangeDays = 30; + private string ExportSenderSubstring = string.Empty; + private readonly HashSet ExportSelectedChannels = []; + private ExportFormat ExportFormat = ExportFormat.Markdown; + private bool ExportRunning; + + // DB-Viewer + Advanced state (was in Database.cs) + private bool ShowAdvanced; + private long DatabaseLastRefreshTicks; + private long DatabaseSize; + private long DatabaseLogSize; + private int DatabaseMessageCount; + + // Channel groupings shared by Cleanup-Breakdown, Retention and Export + // sections. Heading is resolved per-frame so a runtime LanguageChanged + // call updates the labels immediately. 1:1 from Privacy.cs Groups. + private static readonly (Func Heading, ChatType[] Types)[] Groups = + [ + (() => HellionStrings.Privacy_Group_DirectMessages, [ChatType.TellIncoming, ChatType.TellOutgoing]), + (() => HellionStrings.Privacy_Group_PartyAlliance, [ChatType.Party, ChatType.CrossParty, ChatType.Alliance, ChatType.PvpTeam]), + (() => HellionStrings.Privacy_Group_FreeCompany, [ChatType.FreeCompany, ChatType.FreeCompanyAnnouncement, ChatType.FreeCompanyLoginLogout]), + (() => HellionStrings.Privacy_Group_Linkshells, [ + ChatType.Linkshell1, ChatType.Linkshell2, ChatType.Linkshell3, ChatType.Linkshell4, + ChatType.Linkshell5, ChatType.Linkshell6, ChatType.Linkshell7, ChatType.Linkshell8, + ]), + (() => HellionStrings.Privacy_Group_CrossLinkshells, [ + ChatType.CrossLinkshell1, ChatType.CrossLinkshell2, ChatType.CrossLinkshell3, ChatType.CrossLinkshell4, + ChatType.CrossLinkshell5, ChatType.CrossLinkshell6, ChatType.CrossLinkshell7, ChatType.CrossLinkshell8, + ]), + (() => HellionStrings.Privacy_Group_ExtraChat, [ + ChatType.ExtraChatLinkshell1, ChatType.ExtraChatLinkshell2, ChatType.ExtraChatLinkshell3, ChatType.ExtraChatLinkshell4, + ChatType.ExtraChatLinkshell5, ChatType.ExtraChatLinkshell6, ChatType.ExtraChatLinkshell7, ChatType.ExtraChatLinkshell8, + ]), + (() => HellionStrings.Privacy_Group_PublicChat, [ChatType.Say, ChatType.Shout, ChatType.Yell, ChatType.NoviceNetwork, ChatType.CustomEmote, ChatType.StandardEmote]), + (() => HellionStrings.Privacy_Group_SystemLogs, [ + ChatType.System, ChatType.Notice, ChatType.Urgent, ChatType.Echo, + ChatType.NpcDialogue, ChatType.NpcAnnouncement, + ChatType.LootNotice, ChatType.LootRoll, ChatType.RetainerSale, + ChatType.Crafting, ChatType.Gathering, ChatType.Sign, ChatType.RandomNumber, + ]), + ]; + + internal DataManagement(Plugin plugin, Configuration mutable) + { + Plugin = plugin; + Mutable = mutable; + } + + public void Draw(bool changed) + { + // Shift-on-open keeps the Advanced tools available without a permanent + // toggle in the UI, mirroring upstream Chat 2 behaviour. + if (changed) + ShowAdvanced = ImGui.GetIO().KeyShift; + + DrawStorageSection(); + ImGui.Spacing(); + DrawRetentionSection(); + ImGui.Spacing(); + DrawCleanupSection(); + ImGui.Spacing(); + DrawExportSection(); + ImGui.Spacing(); + DrawDatabaseViewerSection(); + ImGui.Spacing(); + DrawAdvancedSection(); + } + + private void DrawStorageSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_Storage_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages); + ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description); + + if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession)) + if (Mutable.LoadPreviousSession) + Mutable.FilterIncludePreviousSessions = true; + ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description); + + if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions)) + if (!Mutable.FilterIncludePreviousSessions) + Mutable.LoadPreviousSession = false; + ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description); + + var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db")); + var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db")); + if (old.Exists || migratedOld.Exists) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + ImGui.TextUnformatted(Language.Options_Database_Old_Heading); + ImGui.Spacing(); + + if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip)) + { + try + { + if (old.Exists) + old.Delete(); + if (migratedOld.Exists) + migratedOld.Delete(); + WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success); + } + catch (Exception e) + { + Plugin.Log.Error(e, "Unable to delete old database"); + WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error); + } + } + } + } + } + + private void DrawRetentionSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_Retention_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGuiUtil.OptionCheckbox( + ref Mutable.RetentionEnabled, + HellionStrings.Retention_Enabled_Name, + HellionStrings.Retention_Enabled_Description); + + using (ImRaii.Disabled(!Mutable.RetentionEnabled)) + { + ImGui.Spacing(); + + var defaultDays = Mutable.RetentionDefaultDays; + if (ImGui.InputInt(HellionStrings.Retention_Default_Label, ref defaultDays)) + Mutable.RetentionDefaultDays = Math.Max(0, defaultDays); + ImGuiUtil.HelpMarker(HellionStrings.Retention_Default_Help); + + ImGui.Spacing(); + + if (ImGui.Button(HellionStrings.Retention_Reset_Spec)) + { + Mutable.RetentionPerChannelDays = + PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value); + } + ImGui.SameLine(); + if (ImGui.Button(HellionStrings.Retention_Clear_Overrides)) + Mutable.RetentionPerChannelDays.Clear(); + + ImGui.Spacing(); + + using (var perChannelTree = ImRaii.TreeNode(HellionStrings.Retention_Tree_Heading)) + { + if (perChannelTree.Success) + { + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + foreach (var (heading, types) in Groups) + { + using var subTree = ImRaii.TreeNode(heading()); + if (!subTree.Success) + continue; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + foreach (var type in types) + { + var hasOverride = Mutable.RetentionPerChannelDays.TryGetValue(type, out var days); + var hasSpecDefault = PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDays); + if (!hasOverride) + days = hasSpecDefault ? specDays : Mutable.RetentionDefaultDays; + + var tag = hasOverride + ? HellionStrings.Retention_Tag_Override + : hasSpecDefault + ? HellionStrings.Retention_Tag_Spec + : HellionStrings.Retention_Tag_Global; + if (ImGui.InputInt($"{type} {tag}##retention-{(int)type}", ref days)) + { + days = Math.Max(0, days); + Mutable.RetentionPerChannelDays[type] = days; + } + + if (hasOverride) + { + ImGui.SameLine(); + if (ImGui.Button($"{HellionStrings.Retention_Reset_Button}##retention-reset-{(int)type}")) + Mutable.RetentionPerChannelDays.Remove(type); + } + } + } + } + } + + ImGui.Spacing(); + + ImGuiUtil.HelpText(HellionStrings.Retention_Help_SavedNote); + ImGui.Spacing(); + + using (ImRaii.Disabled(RetentionRunning)) + { + if (ImGuiUtil.CtrlShiftButton(HellionStrings.Retention_Apply_Label, HellionStrings.Retention_Apply_Tooltip)) + StartRetentionRun(); + } + + if (RetentionRunning) + ImGuiUtil.HelpText(HellionStrings.Retention_Running); + + ImGui.Spacing(); + var lastRun = Plugin.Config.RetentionLastRunAt; + ImGuiUtil.HelpText(lastRun == DateTimeOffset.MinValue + ? HellionStrings.Retention_LastRun_Never + : string.Format(HellionStrings.Retention_LastRun_At, lastRun.ToLocalTime())); + } + } + } + + private void StartRetentionRun() + { + lock (Plugin.RetentionSweepLock) + { + if (Plugin.RetentionSweepRunning) + return; + Plugin.RetentionSweepRunning = true; + } + + var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value); + var defaultDays = Plugin.Config.RetentionDefaultDays; + + new Thread(() => + { + try + { + var deleted = Plugin.MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays); + Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow; + Plugin.SaveConfig(); + + Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages."); + + if (deleted > 0) + { + if (!Plugin.Framework.Run(() => + { + Plugin.MessageManager.ClearAllTabs(); + Plugin.MessageManager.FilterAllTabsAsync(); + }).Wait(TimeSpan.FromSeconds(5))) + { + Plugin.Log.Warning("Retention sweep: framework refresh timed out after 5s."); + } + } + + WrapperUtil.AddNotification(string.Format(HellionStrings.Retention_Success, deleted), NotificationType.Success); + } + catch (Exception e) + { + Plugin.Log.Error(e, "Manual retention run failed"); + WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error); + } + finally + { + lock (Plugin.RetentionSweepLock) + Plugin.RetentionSweepRunning = false; + } + }) { IsBackground = true }.Start(); + } + + private void DrawCleanupSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_Cleanup_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGuiUtil.HelpText(HellionStrings.Cleanup_Help_Intro); + ImGuiUtil.HelpText(HellionStrings.Cleanup_Help_SavedNote); + + ImGui.Spacing(); + + if (CleanupPreviewSnapshot is not null + && !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels)) + { + CleanupPreviewStale = true; + } + + using (var emphasis = CleanupPreviewStale + ? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f }) + : null) + using (ImRaii.Disabled(CleanupRunning)) + { + if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview)) + RefreshCleanupPreview(); + } + + if (CleanupCounts is null) + { + ImGuiUtil.HelpText(HellionStrings.Cleanup_NoPreview); + return; + } + + if (CleanupPreviewStale) + { + ImGui.Spacing(); + ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale); + } + + ImGui.Spacing(); + + using (var staleColor = CleanupPreviewStale + ? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey) + : null) + { + ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount)); + ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount)); + ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount)); + } + + using (var breakdownTree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown)) + { + if (breakdownTree.Success) + { + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + foreach (var (chatType, count) in CleanupCounts.OrderByDescending(p => p.Value)) + { + var name = Enum.IsDefined(typeof(ChatType), (ushort)chatType) + ? ((ChatType)(ushort)chatType).ToString() + : $"Unknown({chatType})"; + var keeps = WouldBeKept(chatType); + var marker = keeps ? HellionStrings.Cleanup_Marker_Keep : HellionStrings.Cleanup_Marker_Delete; + ImGuiUtil.HelpText($"{marker} {name} — {count:N0}"); + } + } + } + + ImGui.Spacing(); + + using (ImRaii.Disabled(CleanupRunning || CleanupDeleteCount == 0)) + { + if (ImGuiUtil.CtrlShiftButton(HellionStrings.Cleanup_Apply_Label, + string.Format(HellionStrings.Cleanup_Apply_Tooltip, CleanupDeleteCount))) + StartCleanup(); + } + + if (CleanupRunning) + ImGuiUtil.HelpText(HellionStrings.Cleanup_Running); + } + } + + private bool WouldBeKept(int chatType) + { + if (!Plugin.Config.PrivacyFilterEnabled) + return true; + if (Plugin.Config.PrivacyPersistChannels.Contains((ChatType)(ushort)chatType)) + return true; + return Plugin.Config.PrivacyPersistUnknownChannels; + } + + private void RefreshCleanupPreview() + { + try + { + CleanupCounts = Plugin.MessageManager.Store.GetMessageCountsByChatType(); + CleanupKeepCount = 0; + CleanupDeleteCount = 0; + foreach (var (chatType, count) in CleanupCounts) + { + if (WouldBeKept(chatType)) + CleanupKeepCount += count; + else + CleanupDeleteCount += count; + } + + CleanupPreviewSnapshot = new HashSet(Mutable.PrivacyPersistChannels); + CleanupPreviewStale = false; + } + catch (Exception e) + { + Plugin.Log.Error(e, "Failed to compute cleanup preview"); + WrapperUtil.AddNotification(HellionStrings.Cleanup_PreviewError, NotificationType.Error); + } + } + + private void StartCleanup() + { + if (CleanupRunning) + return; + + CleanupRunning = true; + var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList(); + + var thread = new Thread(() => + { + try + { + var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed); + Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages"); + + if (!Plugin.Framework.Run(() => + { + Plugin.MessageManager.ClearAllTabs(); + Plugin.MessageManager.FilterAllTabs(); + }).Wait(TimeSpan.FromSeconds(5))) + { + Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s."); + } + + WrapperUtil.AddNotification(string.Format(HellionStrings.Cleanup_Success, deleted), NotificationType.Success); + } + catch (Exception e) + { + Plugin.Log.Error(e, "Privacy cleanup failed"); + WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error); + } + finally + { + CleanupRunning = false; + CleanupCounts = null; + } + }); + thread.IsBackground = true; + thread.Start(); + } + + private void DrawExportSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_Export_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGuiUtil.HelpText(HellionStrings.Export_Help); + + ImGui.Spacing(); + + if (ImGui.InputInt(HellionStrings.Export_Range_Label, ref ExportRangeDays)) + ExportRangeDays = Math.Max(0, ExportRangeDays); + + ImGui.InputText(HellionStrings.Export_Sender_Label, ref ExportSenderSubstring, 256); + + using (var channelsTree = ImRaii.TreeNode(HellionStrings.Export_Channels_Heading)) + { + if (channelsTree.Success) + { + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGuiUtil.HelpText(HellionStrings.Export_Channels_AllOff); + foreach (var (heading, types) in Groups) + { + using var subTree = ImRaii.TreeNode($"{heading()}##export-group-{heading()}"); + if (!subTree.Success) + continue; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + foreach (var type in types) + { + var enabled = ExportSelectedChannels.Contains(type); + if (ImGui.Checkbox($"{type}##export-{(int)type}", ref enabled)) + { + if (enabled) + ExportSelectedChannels.Add(type); + else + ExportSelectedChannels.Remove(type); + } + } + } + } + } + } + + ImGui.Spacing(); + ImGui.TextUnformatted(HellionStrings.Export_Format_Label); + ImGui.SameLine(); + var fmt = (int)ExportFormat; + if (ImGui.RadioButton(HellionStrings.Export_Format_Markdown, ref fmt, (int)ExportFormat.Markdown)) + ExportFormat = ExportFormat.Markdown; + ImGui.SameLine(); + if (ImGui.RadioButton(HellionStrings.Export_Format_Json, ref fmt, (int)ExportFormat.Json)) + ExportFormat = ExportFormat.Json; + ImGui.SameLine(); + if (ImGui.RadioButton(HellionStrings.Export_Format_Csv, ref fmt, (int)ExportFormat.Csv)) + ExportFormat = ExportFormat.Csv; + + ImGui.Spacing(); + + using (ImRaii.Disabled(ExportRunning)) + { + if (ImGui.Button(HellionStrings.Export_Button)) + PromptExport(); + } + + if (ExportRunning) + ImGuiUtil.HelpText(HellionStrings.Export_Running); + } + } + + private void PromptExport() + { + var defaultName = $"hellion-chat-export-{DateTimeOffset.Now:yyyyMMdd-HHmm}"; + var ext = ExportFormat.Extension(); + + Plugin.FileDialogManager.SaveFileDialog( + HellionStrings.Export_Dialog_Title, + ExportFormat.Filter(), + defaultName, + ext, + (success, path) => + { + if (!success || string.IsNullOrWhiteSpace(path)) + return; + StartExport(path); + }); + } + + private void StartExport(string path) + { + if (ExportRunning) + return; + ExportRunning = true; + + var types = ExportSelectedChannels.Count > 0 + ? ExportSelectedChannels.Select(t => (int)(ushort)t).ToList() + : null; + + DateTimeOffset? from = ExportRangeDays > 0 + ? DateTimeOffset.UtcNow.AddDays(-ExportRangeDays) + : null; + + var senderSubstring = string.IsNullOrWhiteSpace(ExportSenderSubstring) ? null : ExportSenderSubstring.Trim(); + var format = ExportFormat; + var filterDesc = new MessageExporter.FilterDescription(types, from, null, senderSubstring); + + new Thread(() => + { + try + { + using var enumerator = Plugin.MessageManager.Store.StreamForExport(types, from, null); + var written = MessageExporter.ExportToFile(path, format, enumerator, filterDesc); + + if (written > 0) + WrapperUtil.AddNotification(string.Format(HellionStrings.Export_Success, written, path), NotificationType.Success); + else + WrapperUtil.AddNotification(HellionStrings.Export_Empty, NotificationType.Info); + } + catch (Exception e) + { + Plugin.Log.Error(e, "Export failed"); + WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error); + } + finally + { + ExportRunning = false; + } + }) { IsBackground = true }.Start(); + } + + private void DrawDatabaseViewerSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_DbViewer_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + if (DatabaseLastRefreshTicks + 5 * 1000 < Environment.TickCount64) + { + DatabaseSize = Plugin.MessageManager.Store.DatabaseSize(); + DatabaseLogSize = Plugin.MessageManager.Store.DatabaseLogSize(); + DatabaseMessageCount = Plugin.MessageManager.Store.MessageCount(); + DatabaseLastRefreshTicks = Environment.TickCount64; + } + + ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Path, MessageManager.DatabasePath())); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var path = Path.GetDirectoryName(MessageManager.DatabasePath()); + ImGui.SetClipboardText(path); + WrapperUtil.AddNotification(Language.Options_Database_Metadata_CopyConfigPathNotification, NotificationType.Info); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + ImGuiUtil.Tooltip(Language.Options_Database_Metadata_CopyConfigPath); + } + + ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize))); + if (ImGui.IsItemHovered()) + ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseSize)); + + ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize))); + if (ImGui.IsItemHovered()) + ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseLogSize)); + + ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount)); + + if (ImGuiUtil.CtrlShiftButton(Language.Options_ClearDatabase_Button, Language.Options_ClearDatabase_Tooltip)) + { + Plugin.Log.Warning("Clearing messages from database"); + Plugin.MessageManager.Store.ClearMessages(); + Plugin.MessageManager.ClearAllTabs(); + + DatabaseLastRefreshTicks = 0; + WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info); + } + } + } + + private void DrawAdvancedSection() + { + if (!ShowAdvanced) + return; + + using var tree = ImRaii.TreeNode(HellionStrings.Settings_DataManagement_Advanced_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + using var wrap = ImRaii.TextWrapPos(0.0f); + + ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning); + if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()")) + Plugin.MessageManager.Store.PerformMaintenance(); + + if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()")) + { + Plugin.MessageManager.ClearAllTabs(); + Plugin.MessageManager.FilterAllTabsAsync(); + } + + if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)")) + new Thread(() => InsertMessages(10_000)).Start(); + } + } + + private void InsertMessages(int count) + { + Plugin.Log.Info($"Inserting {count} messages due to user request"); + + var stopwatch = Stopwatch.StartNew(); + var playerName = Plugin.PlayerState.CharacterName; + var worldId = Plugin.PlayerState.HomeWorld.ValueNullable?.RowId ?? 0; + var senderSource = new SeStringBuilder() + .AddText("<") + .Add(new PlayerPayload(playerName, worldId)) + .AddText("Random Message") + .Add(RawPayload.LinkTerminator) + .AddText(">: ") + .Build(); + var senderChunks = ChunkUtil.ToChunks(senderSource, ChunkSource.Sender, ChatType.Debug).ToList(); + var messages = new List(count); + for (var i = 0; i < count; i++) + { + var contentSource = new SeStringBuilder() + .AddText("Random message payload - ") + .AddItalics(Guid.NewGuid().ToString()) + .Build(); + var contentChunks = ChunkUtil.ToChunks(contentSource, ChunkSource.Content, ChatType.Debug).ToList(); + + var chatCode = new ChatCode(XivChatType.Say, 0, 0); + messages.Add(new Message( + Guid.NewGuid(), + Plugin.MessageManager.CurrentContentId, + Plugin.MessageManager.CurrentContentId, + DateTimeOffset.UtcNow, + chatCode, + senderChunks, + contentChunks, + senderSource, + contentSource, + Guid.Empty + )); + } + + var elapsedTicks = stopwatch.ElapsedTicks; + stopwatch.Stop(); + Plugin.Log.Info($"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); + + stopwatch = Stopwatch.StartNew(); + foreach (var message in messages) + Plugin.MessageManager.Store.UpsertMessage(message); + + elapsedTicks = stopwatch.ElapsedTicks; + stopwatch.Stop(); + Plugin.Log.Info($"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); + + Plugin.Framework.Run(() => + { + stopwatch = Stopwatch.StartNew(); + Plugin.MessageManager.ClearAllTabs(); + elapsedTicks = stopwatch.ElapsedTicks; + stopwatch.Stop(); + Plugin.Log.Info($"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); + }).Wait(); + + Plugin.Framework.Run(() => + { + stopwatch = Stopwatch.StartNew(); + Plugin.MessageManager.FilterAllTabs(); + elapsedTicks = stopwatch.ElapsedTicks; + stopwatch.Stop(); + Plugin.Log.Info($"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); + }).Wait(); + } +} diff --git a/HellionChat/Ui/SettingsTabs/Database.cs b/HellionChat/Ui/SettingsTabs/Database.cs deleted file mode 100755 index fa93818..0000000 --- a/HellionChat/Ui/SettingsTabs/Database.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Diagnostics; -using HellionChat.Code; -using HellionChat.Resources; -using HellionChat.Util; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Bindings.ImGui; -using Dalamud.Game.Text; - -namespace HellionChat.Ui.SettingsTabs; - -internal sealed class Database : ISettingsTab -{ - private Plugin Plugin { get; } - private Configuration Mutable { get; } - - public string Name => HellionStrings.Settings_Tab_Database + "###tabs-database"; - - internal Database(Plugin plugin, Configuration mutable) - { - Plugin = plugin; - Mutable = mutable; - } - - private bool ShowAdvanced; - - private long DatabaseLastRefreshTicks; - private long DatabaseSize; - private long DatabaseLogSize; - private int DatabaseMessageCount; - - public void Draw(bool changed) - { - // Shift-on-open keeps the Advanced tools available without a permanent - // toggle in the UI, mirroring upstream Chat 2 behaviour. - if (changed) - ShowAdvanced = ImGui.GetIO().KeyShift; - - DrawStorageSection(); - ImGui.Spacing(); - DrawViewerSection(); - ImGui.Spacing(); - DrawStatsSection(); - } - - private void DrawStorageSection() - { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Storage_Heading); - if (!tree.Success) - return; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages); - ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description); - - if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession)) - if (Mutable.LoadPreviousSession) - Mutable.FilterIncludePreviousSessions = true; - ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description); - - if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions)) - if (!Mutable.FilterIncludePreviousSessions) - Mutable.LoadPreviousSession = false; - ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description); - - var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db")); - var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db")); - if (old.Exists || migratedOld.Exists) - { - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - ImGui.TextUnformatted(Language.Options_Database_Old_Heading); - ImGui.Spacing(); - - if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip)) - { - try - { - // Delete both legacy files in one click — the previous if/else - // left the second file behind when both happened to exist. - if (old.Exists) - old.Delete(); - if (migratedOld.Exists) - migratedOld.Delete(); - WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success); - } - catch (Exception e) - { - Plugin.Log.Error(e, "Unable to delete old database"); - WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error); - } - } - } - } - } - - private void DrawViewerSection() - { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Viewer_Heading); - if (!tree.Success) - return; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - // Refresh the database size and message count every 5 seconds to avoid - // constant stat calls and spamming the database. - if (DatabaseLastRefreshTicks + 5 * 1000 < Environment.TickCount64) - { - DatabaseSize = Plugin.MessageManager.Store.DatabaseSize(); - DatabaseLogSize = Plugin.MessageManager.Store.DatabaseLogSize(); - DatabaseMessageCount = Plugin.MessageManager.Store.MessageCount(); - DatabaseLastRefreshTicks = Environment.TickCount64; - } - - // Copy the directory path instead of the file path so people can - // paste it into their file explorer. - ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Path, MessageManager.DatabasePath())); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - var path = Path.GetDirectoryName(MessageManager.DatabasePath()); - ImGui.SetClipboardText(path); - WrapperUtil.AddNotification(Language.Options_Database_Metadata_CopyConfigPathNotification, NotificationType.Info); - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGuiUtil.Tooltip(Language.Options_Database_Metadata_CopyConfigPath); - } - - ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize))); - if (ImGui.IsItemHovered()) - ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseSize)); - - ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize))); - if (ImGui.IsItemHovered()) - ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseLogSize)); - - ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount)); - - if (ImGuiUtil.CtrlShiftButton(Language.Options_ClearDatabase_Button, Language.Options_ClearDatabase_Tooltip)) - { - Plugin.Log.Warning("Clearing messages from database"); - Plugin.MessageManager.Store.ClearMessages(); - Plugin.MessageManager.ClearAllTabs(); - - // Refresh on next draw - DatabaseLastRefreshTicks = 0; - WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info); - } - } - } - - private void DrawStatsSection() - { - if (!ShowAdvanced) - return; - - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Database_Stats_Heading); - if (!tree.Success) - return; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - using var wrap = ImRaii.TextWrapPos(0.0f); - - ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning); - if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()")) - Plugin.MessageManager.Store.PerformMaintenance(); - - if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()")) - { - Plugin.MessageManager.ClearAllTabs(); - Plugin.MessageManager.FilterAllTabsAsync(); - } - - if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)")) - new Thread(() => InsertMessages(10_000)).Start(); - } - } - - private void InsertMessages(int count) - { - Plugin.Log.Info($"Inserting {count} messages due to user request"); - - // Generate - var stopwatch = Stopwatch.StartNew(); - var playerName = Plugin.PlayerState.CharacterName; - var worldId = Plugin.PlayerState.HomeWorld.ValueNullable?.RowId ?? 0; - var senderSource = new SeStringBuilder() - .AddText("<") - .Add(new PlayerPayload(playerName, worldId)) - .AddText("Random Message") - .Add(RawPayload.LinkTerminator) - .AddText(">: ") - .Build(); - var senderChunks = ChunkUtil.ToChunks(senderSource, ChunkSource.Sender, ChatType.Debug).ToList(); - var messages = new List(count); - for (var i = 0; i < count; i++) - { - var contentSource = new SeStringBuilder() - .AddText("Random message payload - ") - .AddItalics(Guid.NewGuid().ToString()) - .Build(); - var contentChunks = ChunkUtil.ToChunks(contentSource, ChunkSource.Content, ChatType.Debug).ToList(); - - var chatCode = new ChatCode(XivChatType.Say, 0, 0); - messages.Add(new Message( - Guid.NewGuid(), - Plugin.MessageManager.CurrentContentId, - Plugin.MessageManager.CurrentContentId, - DateTimeOffset.UtcNow, - chatCode, - senderChunks, - contentChunks, - senderSource, - contentSource, - Guid.Empty - )); - } - - var elapsedTicks = stopwatch.ElapsedTicks; - stopwatch.Stop(); - Plugin.Log.Info($"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); - - // Insert - stopwatch = Stopwatch.StartNew(); - foreach (var message in messages) - Plugin.MessageManager.Store.UpsertMessage(message); - - elapsedTicks = stopwatch.ElapsedTicks; - stopwatch.Stop(); - Plugin.Log.Info($"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); - - // Clear tabs during framework frame - Plugin.Framework.Run(() => - { - stopwatch = Stopwatch.StartNew(); - Plugin.MessageManager.ClearAllTabs(); - elapsedTicks = stopwatch.ElapsedTicks; - stopwatch.Stop(); - Plugin.Log.Info($"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); - }).Wait(); - - // Fetch and filter during framework frame - Plugin.Framework.Run(() => - { - stopwatch = Stopwatch.StartNew(); - // Intentionally synchronous - Plugin.MessageManager.FilterAllTabs(); - elapsedTicks = stopwatch.ElapsedTicks; - stopwatch.Stop(); - Plugin.Log.Info($"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"); - }).Wait(); - } -} diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/FontsAndColours.cs similarity index 64% rename from HellionChat/Ui/SettingsTabs/Appearance.cs rename to HellionChat/Ui/SettingsTabs/FontsAndColours.cs index 031ab0b..1d6dd26 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/FontsAndColours.cs @@ -4,21 +4,20 @@ using HellionChat.Util; using Dalamud; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; -using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Bindings.ImGui; namespace HellionChat.Ui.SettingsTabs; -internal sealed class Appearance : ISettingsTab +internal sealed class FontsAndColours : ISettingsTab { private Plugin Plugin { get; } private Configuration Mutable { get; } - public string Name => HellionStrings.Settings_Tab_Appearance + "###tabs-appearance"; + public string Name => HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours"; - internal Appearance(Plugin plugin, Configuration mutable) + internal FontsAndColours(Plugin plugin, Configuration mutable) { Plugin = plugin; Mutable = mutable; @@ -26,94 +25,27 @@ internal sealed class Appearance : ISettingsTab public void Draw(bool changed) { - DrawThemeSection(); - ImGui.Spacing(); DrawFontsSection(); ImGui.Spacing(); DrawColoursSection(); - ImGui.Spacing(); - DrawTimestampsSection(); - } - - private void DrawThemeSection() - { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Theme_Heading); - if (!tree.Success) - { - return; - } - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - // v1.2.0 — Legacy HellionThemeEnabled/HellionThemeWindowOpacity-Bindings - // entfernt. Theme-Auswahl + globale Window-Opacity leben jetzt in - // Settings → Themes (eingeführt mit v1.1.0). Hier verbleibt nur der - // klassische OverrideStyle-Toggle plus der Bestand-WindowAlpha-Slider - // für das Chat-Log-Fenster. - ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle); - ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc); - - if (Mutable.OverrideStyle) - { - DrawStyleCombo(); - } - - ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); - } - } - - private void DrawStyleCombo() - { - var styles = StyleModel.GetConfiguredStyles(); - if (styles == null) - { - ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable); - return; - } - - var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected; - using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle); - if (!combo) - { - return; - } - - foreach (var style in styles) - { - if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name)) - { - Mutable.ChosenStyle = style.Name; - } - } } private void DrawFontsSection() { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Fonts_Heading); + using var tree = ImRaii.TreeNode(HellionStrings.Settings_FontsAndColours_Fonts_Heading); if (!tree.Success) - { return; - } using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { if (ImGui.Checkbox(HellionStrings.Theme_UseHellionFont_Name, ref Mutable.UseHellionFont)) { - // Mutex with the Bestand custom-font stack. Leaving FontsEnabled - // checked alongside UseHellionFont made both checkboxes look - // active even though the lower stack was greyed out, which - // confused the user during the v0.5.0 walkthrough. if (Mutable.UseHellionFont) Mutable.FontsEnabled = false; } ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description); ImGui.Spacing(); - // v1.2.0 — Schriftgröße muss auch bei aktiver Hellion-Schrift - // editierbar sein (Exo 2 ist Variable-Font, FontSizeV2 wird in - // FontManager als SizePt angewendet). Disabled-Wrap nur noch - // um den Bestand-Custom-Font-Stack (FontsEnabled-Toggle und - // die Font-Chooser) — der ist weiter exclusive zu HellionFont. if (Mutable.UseHellionFont) { ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2); @@ -154,7 +86,6 @@ internal sealed class Appearance : ISettingsTab ImGuiUtil.WarningText(Language.Options_Font_Warning); ImGui.Spacing(); - // LocaleNames being null means it is likely a game font which all support JP symbols. var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref unused, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを"); japaneseChooser?.ResultTask.ContinueWith(r => { @@ -215,11 +146,9 @@ internal sealed class Appearance : ISettingsTab private void DrawColoursSection() { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Colours_Heading); + using var tree = ImRaii.TreeNode(HellionStrings.Settings_FontsAndColours_Colours_Heading); if (!tree.Success) - { return; - } using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { @@ -266,9 +195,6 @@ internal sealed class Appearance : ISettingsTab } } - // Hellion Chat — v0.6.0 preset-buttons row above the per-channel colour - // editors. Apply is immediate and overwrites every channel covered by - // the preset; channels not in the preset keep their current colour. private void DrawColourPresetButtons() { var first = true; @@ -282,7 +208,6 @@ internal sealed class Appearance : ISettingsTab if (preset.IsBrandPreset) { - // Hellion-Brand visuell hervorheben — blau-violetter Frame-Akzent var border = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(255, 128, 200)); var btn = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(74, 42, 106)); ImGui.PushStyleColor(ImGuiCol.Border, new System.Numerics.Vector4(border.X, border.Y, border.Z, 1f)); @@ -303,9 +228,6 @@ internal sealed class Appearance : ISettingsTab } } - // Localized label for a preset; falls back to DisplayName if the i18n - // key is missing (defensive — the resource manager returns the key - // string itself when a lookup fails). private static string GetPresetLabel(ChatColourPreset preset) { var localized = HellionStrings.ResourceManager.GetString(preset.LocalizationKey, HellionStrings.Culture); @@ -322,36 +244,4 @@ internal sealed class Appearance : ISettingsTab GlobalParametersCache.Refresh(); Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}"); } - - private void DrawTimestampsSection() - { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading); - if (!tree.Success) - { - return; - } - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGui.Checkbox(Language.Options_PrettierTimestamps_Name, ref Mutable.PrettierTimestamps); - ImGuiUtil.HelpMarker(Language.Options_PrettierTimestamps_Description); - - if (Mutable.PrettierTimestamps) - { - ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty); - ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description); - - // v1.2.0 — Card-Rows als Default. Compact-Density schaltet auf den - // klassischen Single-Line-Mode `[HH:mm] Sender: Text` zurück. - ImGui.Checkbox(HellionStrings.Appearance_UseCompactDensity_Name, ref Mutable.UseCompactDensity); - ImGuiUtil.HelpMarker(HellionStrings.Appearance_UseCompactDensity_Description); - - ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps); - ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description); - } - - ImGui.Checkbox(Language.Options_Use24HourClock_Name, ref Mutable.Use24HourClock); - ImGuiUtil.HelpMarker(Language.Options_Use24HourClock_Description); - } - } } diff --git a/HellionChat/Ui/SettingsTabs/General.cs b/HellionChat/Ui/SettingsTabs/General.cs index 663a6b9..8513b5e 100644 --- a/HellionChat/Ui/SettingsTabs/General.cs +++ b/HellionChat/Ui/SettingsTabs/General.cs @@ -51,6 +51,28 @@ internal sealed class General : ISettingsTab ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name); ImGui.SetNextItemWidth(-1); ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward); + + ImGui.Spacing(); + + using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name())) + { + if (combo.Success) + { + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode)) + { + Mutable.KeybindMode = mode; + } + + if (ImGui.IsItemHovered()) + { + ImGuiUtil.Tooltip(mode.Tooltip() ?? ""); + } + } + } + } + ImGuiUtil.HelpMarker(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName)); } } @@ -133,27 +155,6 @@ internal sealed class General : ISettingsTab ImGuiUtil.HelpMarker(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName)); ImGui.Spacing(); - using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name())) - { - if (combo.Success) - { - foreach (var mode in Enum.GetValues()) - { - if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode)) - { - Mutable.KeybindMode = mode; - } - - if (ImGui.IsItemHovered()) - { - ImGuiUtil.Tooltip(mode.Tooltip() ?? ""); - } - } - } - } - ImGuiUtil.HelpMarker(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName)); - ImGui.Spacing(); - ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate); ImGuiUtil.HelpMarker(Language.Options_SortAutoTranslate_Description); } diff --git a/HellionChat/Ui/SettingsTabs/Privacy.cs b/HellionChat/Ui/SettingsTabs/Privacy.cs index 071155e..b38b367 100644 --- a/HellionChat/Ui/SettingsTabs/Privacy.cs +++ b/HellionChat/Ui/SettingsTabs/Privacy.cs @@ -1,11 +1,7 @@ using HellionChat.Code; -using HellionChat.Export; using HellionChat.Privacy; using HellionChat.Resources; using HellionChat.Util; -using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Bindings.ImGui; @@ -52,25 +48,6 @@ internal sealed class Privacy : ISettingsTab ]), ]; - private Dictionary? CleanupCounts; - private long CleanupKeepCount; - private long CleanupDeleteCount; - private bool CleanupRunning; - private bool CleanupPreviewStale; - private HashSet? CleanupPreviewSnapshot; - - // The retention-running state lives on Plugin so the auto-sweep and - // this manual button see the same flag. UI reads stay lock-free - // because ImGui is single-threaded and bool reads are atomic in .NET. - private bool RetentionRunning => Plugin.RetentionSweepRunning; - - // Export form state - private int ExportRangeDays = 30; - private string ExportSenderSubstring = string.Empty; - private readonly HashSet ExportSelectedChannels = []; - private ExportFormat ExportFormat = ExportFormat.Markdown; - private bool ExportRunning; - public void Draw(bool changed) { if (ImGui.Button(HellionStrings.Wizard_Reopen_Button)) @@ -78,51 +55,6 @@ internal sealed class Privacy : ISettingsTab ImGui.Spacing(); DrawPrivacyFilterSection(); - - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - DrawRetentionSection(); - - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - DrawCleanupSection(); - - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - DrawExportSection(); - - ImGui.Spacing(); - ImGui.Separator(); - ImGui.Spacing(); - - DrawAutoTellTabsPreloadSection(); - } - - private void DrawAutoTellTabsPreloadSection() - { - using var tree = ImRaii.TreeNode(HellionStrings.Privacy_AutoTellTabs_Section_Title); - if (!tree.Success) - return; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - var preload = Mutable.AutoTellTabsHistoryPreload; - ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); - if (ImGui.SliderInt(HellionStrings.Privacy_AutoTellTabs_Preload_Name, ref preload, 0, 100)) - { - Mutable.AutoTellTabsHistoryPreload = preload; - } - ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description); - - ImGui.Spacing(); - ImGuiUtil.HelpText(HellionStrings.Privacy_AutoTellTabs_Preload_Hint); - } } private void DrawPrivacyFilterSection() @@ -200,459 +132,4 @@ internal sealed class Privacy : ISettingsTab } } } - - private void DrawExportSection() - { - ImGui.TextUnformatted(HellionStrings.Export_Heading); - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGuiUtil.HelpText(HellionStrings.Export_Help); - - ImGui.Spacing(); - - if (ImGui.InputInt(HellionStrings.Export_Range_Label, ref ExportRangeDays)) - ExportRangeDays = Math.Max(0, ExportRangeDays); - - ImGui.InputText(HellionStrings.Export_Sender_Label, ref ExportSenderSubstring, 256); - - using (var tree = ImRaii.TreeNode(HellionStrings.Export_Channels_Heading)) - { - if (tree.Success) - { - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGuiUtil.HelpText(HellionStrings.Export_Channels_AllOff); - foreach (var (heading, types) in Groups) - { - using var subTree = ImRaii.TreeNode($"{heading()}##export-group-{heading()}"); - if (!subTree.Success) - continue; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - foreach (var type in types) - { - var enabled = ExportSelectedChannels.Contains(type); - if (ImGui.Checkbox($"{type}##export-{(int)type}", ref enabled)) - { - if (enabled) - ExportSelectedChannels.Add(type); - else - ExportSelectedChannels.Remove(type); - } - } - } - } - } - } - - ImGui.Spacing(); - ImGui.TextUnformatted(HellionStrings.Export_Format_Label); - ImGui.SameLine(); - var fmt = (int)ExportFormat; - if (ImGui.RadioButton(HellionStrings.Export_Format_Markdown, ref fmt, (int)ExportFormat.Markdown)) - ExportFormat = ExportFormat.Markdown; - ImGui.SameLine(); - if (ImGui.RadioButton(HellionStrings.Export_Format_Json, ref fmt, (int)ExportFormat.Json)) - ExportFormat = ExportFormat.Json; - ImGui.SameLine(); - if (ImGui.RadioButton(HellionStrings.Export_Format_Csv, ref fmt, (int)ExportFormat.Csv)) - ExportFormat = ExportFormat.Csv; - - ImGui.Spacing(); - - using (ImRaii.Disabled(ExportRunning)) - { - if (ImGui.Button(HellionStrings.Export_Button)) - PromptExport(); - } - - if (ExportRunning) - ImGuiUtil.HelpText(HellionStrings.Export_Running); - } - } - - private void PromptExport() - { - var defaultName = $"hellion-chat-export-{DateTimeOffset.Now:yyyyMMdd-HHmm}"; - var ext = ExportFormat.Extension(); - - Plugin.FileDialogManager.SaveFileDialog( - HellionStrings.Export_Dialog_Title, - ExportFormat.Filter(), - defaultName, - ext, - (success, path) => - { - if (!success || string.IsNullOrWhiteSpace(path)) - return; - StartExport(path); - }); - } - - private void StartExport(string path) - { - if (ExportRunning) - return; - ExportRunning = true; - - var types = ExportSelectedChannels.Count > 0 - ? ExportSelectedChannels.Select(t => (int)(ushort)t).ToList() - : null; - - DateTimeOffset? from = ExportRangeDays > 0 - ? DateTimeOffset.UtcNow.AddDays(-ExportRangeDays) - : null; - - var senderSubstring = string.IsNullOrWhiteSpace(ExportSenderSubstring) ? null : ExportSenderSubstring.Trim(); - var format = ExportFormat; - var filterDesc = new MessageExporter.FilterDescription(types, from, null, senderSubstring); - - new Thread(() => - { - try - { - using var enumerator = Plugin.MessageManager.Store.StreamForExport(types, from, null); - var written = MessageExporter.ExportToFile(path, format, enumerator, filterDesc); - - if (written > 0) - WrapperUtil.AddNotification(string.Format(HellionStrings.Export_Success, written, path), NotificationType.Success); - else - WrapperUtil.AddNotification(HellionStrings.Export_Empty, NotificationType.Info); - } - catch (Exception e) - { - Plugin.Log.Error(e, "Export failed"); - WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error); - } - finally - { - ExportRunning = false; - } - }) { IsBackground = true }.Start(); - } - - private void DrawRetentionSection() - { - ImGui.TextUnformatted(HellionStrings.Retention_Heading); - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGuiUtil.OptionCheckbox( - ref Mutable.RetentionEnabled, - HellionStrings.Retention_Enabled_Name, - HellionStrings.Retention_Enabled_Description); - - using (ImRaii.Disabled(!Mutable.RetentionEnabled)) - { - ImGui.Spacing(); - - var defaultDays = Mutable.RetentionDefaultDays; - if (ImGui.InputInt(HellionStrings.Retention_Default_Label, ref defaultDays)) - Mutable.RetentionDefaultDays = Math.Max(0, defaultDays); - ImGuiUtil.HelpMarker(HellionStrings.Retention_Default_Help); - - ImGui.Spacing(); - - if (ImGui.Button(HellionStrings.Retention_Reset_Spec)) - { - Mutable.RetentionPerChannelDays = - PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value); - } - ImGui.SameLine(); - if (ImGui.Button(HellionStrings.Retention_Clear_Overrides)) - Mutable.RetentionPerChannelDays.Clear(); - - ImGui.Spacing(); - - using (var tree = ImRaii.TreeNode(HellionStrings.Retention_Tree_Heading)) - { - if (tree.Success) - { - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - foreach (var (heading, types) in Groups) - { - using var subTree = ImRaii.TreeNode(heading()); - if (!subTree.Success) - continue; - - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - foreach (var type in types) - { - var hasOverride = Mutable.RetentionPerChannelDays.TryGetValue(type, out var days); - var hasSpecDefault = PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDays); - if (!hasOverride) - days = hasSpecDefault ? specDays : Mutable.RetentionDefaultDays; - - var tag = hasOverride - ? HellionStrings.Retention_Tag_Override - : hasSpecDefault - ? HellionStrings.Retention_Tag_Spec - : HellionStrings.Retention_Tag_Global; - if (ImGui.InputInt($"{type} {tag}##retention-{(int)type}", ref days)) - { - days = Math.Max(0, days); - Mutable.RetentionPerChannelDays[type] = days; - } - - if (hasOverride) - { - ImGui.SameLine(); - if (ImGui.Button($"{HellionStrings.Retention_Reset_Button}##retention-reset-{(int)type}")) - Mutable.RetentionPerChannelDays.Remove(type); - } - } - } - } - } - - ImGui.Spacing(); - - ImGuiUtil.HelpText(HellionStrings.Retention_Help_SavedNote); - ImGui.Spacing(); - - using (ImRaii.Disabled(RetentionRunning)) - { - if (ImGuiUtil.CtrlShiftButton(HellionStrings.Retention_Apply_Label, HellionStrings.Retention_Apply_Tooltip)) - StartRetentionRun(); - } - - if (RetentionRunning) - ImGuiUtil.HelpText(HellionStrings.Retention_Running); - - ImGui.Spacing(); - var lastRun = Plugin.Config.RetentionLastRunAt; - ImGuiUtil.HelpText(lastRun == DateTimeOffset.MinValue - ? HellionStrings.Retention_LastRun_Never - : string.Format(HellionStrings.Retention_LastRun_At, lastRun.ToLocalTime())); - } - } - } - - private void StartRetentionRun() - { - // Take the shared retention lock so we cannot fight the auto-sweep - // for the database connection. If the auto-sweep is already in - // flight we just bail — the user can press the button again once - // it finishes. - lock (Plugin.RetentionSweepLock) - { - if (Plugin.RetentionSweepRunning) - return; - Plugin.RetentionSweepRunning = true; - } - - var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value); - var defaultDays = Plugin.Config.RetentionDefaultDays; - - new Thread(() => - { - try - { - var deleted = Plugin.MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays); - Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow; - Plugin.SaveConfig(); - - Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages."); - - if (deleted > 0) - { - // Bound the wait so a hung framework tick can't deadlock - // the background retention worker. Five seconds is well - // beyond a normal frame; if we time out we log and let - // the next FilterAllTabsAsync call recover the state. - if (!Plugin.Framework.Run(() => - { - Plugin.MessageManager.ClearAllTabs(); - Plugin.MessageManager.FilterAllTabsAsync(); - }).Wait(TimeSpan.FromSeconds(5))) - { - Plugin.Log.Warning("Retention sweep: framework refresh timed out after 5s."); - } - } - - WrapperUtil.AddNotification(string.Format(HellionStrings.Retention_Success, deleted), NotificationType.Success); - } - catch (Exception e) - { - Plugin.Log.Error(e, "Manual retention run failed"); - WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error); - } - finally - { - lock (Plugin.RetentionSweepLock) - Plugin.RetentionSweepRunning = false; - } - }) { IsBackground = true }.Start(); - } - - private void DrawCleanupSection() - { - ImGui.TextUnformatted(HellionStrings.Cleanup_Heading); - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - { - ImGuiUtil.HelpText(HellionStrings.Cleanup_Help_Intro); - ImGuiUtil.HelpText(HellionStrings.Cleanup_Help_SavedNote); - - ImGui.Spacing(); - - // Drift-detection between the snapshot taken at last refresh - // and the current Mutable whitelist. Cleanup itself runs on - // the SAVED policy (Cleanup_Help_SavedNote covers that), but - // the user usually expects "the preview reflects what I just - // ticked" — so we surface the divergence instead of silently - // showing stale numbers. - if (CleanupPreviewSnapshot is not null - && !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels)) - { - CleanupPreviewStale = true; - } - - using (var emphasis = CleanupPreviewStale - ? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f }) - : null) - using (ImRaii.Disabled(CleanupRunning)) - { - if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview)) - RefreshCleanupPreview(); - } - - if (CleanupCounts is null) - { - ImGuiUtil.HelpText(HellionStrings.Cleanup_NoPreview); - return; - } - - if (CleanupPreviewStale) - { - ImGui.Spacing(); - ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale); - } - - ImGui.Spacing(); - - using (var staleColor = CleanupPreviewStale - ? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey) - : null) - { - ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount)); - ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount)); - ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount)); - } - - using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown)) - { - if (tree.Success) - { - using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) - foreach (var (chatType, count) in CleanupCounts.OrderByDescending(p => p.Value)) - { - var name = Enum.IsDefined(typeof(ChatType), (ushort)chatType) - ? ((ChatType)(ushort)chatType).ToString() - : $"Unknown({chatType})"; - var keeps = WouldBeKept(chatType); - var marker = keeps ? HellionStrings.Cleanup_Marker_Keep : HellionStrings.Cleanup_Marker_Delete; - ImGuiUtil.HelpText($"{marker} {name} — {count:N0}"); - } - } - } - - ImGui.Spacing(); - - using (ImRaii.Disabled(CleanupRunning || CleanupDeleteCount == 0)) - { - if (ImGuiUtil.CtrlShiftButton(HellionStrings.Cleanup_Apply_Label, - string.Format(HellionStrings.Cleanup_Apply_Tooltip, CleanupDeleteCount))) - StartCleanup(); - } - - if (CleanupRunning) - ImGuiUtil.HelpText(HellionStrings.Cleanup_Running); - } - } - - private bool WouldBeKept(int chatType) - { - if (!Plugin.Config.PrivacyFilterEnabled) - return true; - if (Plugin.Config.PrivacyPersistChannels.Contains((ChatType)(ushort)chatType)) - return true; - return Plugin.Config.PrivacyPersistUnknownChannels; - } - - private void RefreshCleanupPreview() - { - try - { - CleanupCounts = Plugin.MessageManager.Store.GetMessageCountsByChatType(); - CleanupKeepCount = 0; - CleanupDeleteCount = 0; - foreach (var (chatType, count) in CleanupCounts) - { - if (WouldBeKept(chatType)) - CleanupKeepCount += count; - else - CleanupDeleteCount += count; - } - - // Snapshot the whitelist as it stood at preview-time so the - // render pass can flag the user about subsequent edits. Only - // updated on success — if the preview throws, the previous - // snapshot stays in place so stale-detection keeps working. - CleanupPreviewSnapshot = new HashSet(Mutable.PrivacyPersistChannels); - CleanupPreviewStale = false; - } - catch (Exception e) - { - Plugin.Log.Error(e, "Failed to compute cleanup preview"); - WrapperUtil.AddNotification(HellionStrings.Cleanup_PreviewError, NotificationType.Error); - } - } - - private void StartCleanup() - { - if (CleanupRunning) - return; - - CleanupRunning = true; - var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList(); - - var thread = new Thread(() => - { - try - { - var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed); - Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages"); - - // Bound the wait so a hung framework tick can't deadlock - // the background cleanup worker. See the matching comment in - // the retention path above for rationale. - // Note: FilterAllTabs() is called synchronously instead of - // FilterAllTabsAsync() — the async variant fires-and-forgets - // a Task.Run, so the .Wait() would return before the filter - // pass actually finishes. See AUDIT-2026-05-05 [QUAL-02]. - if (!Plugin.Framework.Run(() => - { - Plugin.MessageManager.ClearAllTabs(); - Plugin.MessageManager.FilterAllTabs(); - }).Wait(TimeSpan.FromSeconds(5))) - { - Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s."); - } - - WrapperUtil.AddNotification(string.Format(HellionStrings.Cleanup_Success, deleted), NotificationType.Success); - } - catch (Exception e) - { - Plugin.Log.Error(e, "Privacy cleanup failed"); - WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error); - } - finally - { - CleanupRunning = false; - CleanupCounts = null; - } - }); - // Background thread so a still-running cleanup doesn't hold up FFXIV exit. - thread.IsBackground = true; - thread.Start(); - } } diff --git a/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs new file mode 100644 index 0000000..2bd2979 --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/ThemeAndLayout.cs @@ -0,0 +1,286 @@ +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 ThemeAndLayout : ISettingsTab +{ + private Plugin Plugin { get; } + private Configuration Mutable { get; } + + private string? _applyDismissedFor; + + public string Name => HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout"; + + internal ThemeAndLayout(Plugin plugin, Configuration mutable) + { + Plugin = plugin; + Mutable = mutable; + } + + public void Draw(bool changed) + { + DrawThemeSection(); + ImGui.Spacing(); + DrawWindowStyleSection(); + ImGui.Spacing(); + DrawTimestampStyleSection(); + } + + private void DrawThemeSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_ThemeAndLayout_Theme_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + 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 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; + + var list = themes.ToList(); + for (var i = 0; i < list.Count; i++) + { + DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight); + + if ((i + 1) % columns != 0 && i != list.Count - 1) + ImGui.SameLine(); + } + } + + private void DrawThemeCard(Theme theme, string activeSlug, float w, float h) + { + 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); + } + + var mockupOrigin = cursorBefore + new Vector2(12f, 12f); + var mockupSize = new Vector2(w - 24f, 60f); + ThemeMockup.Draw(mockupOrigin, mockupSize, theme); + + 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; + } + } + + private void DrawChatColorsApplyBanner(Theme active) + { + if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors) + return; + + if (_applyDismissedFor == active.Slug) + return; + + var alreadyMatching = themeChatColors.Channels.All(kvp => + Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value); + if (alreadyMatching) + return; + + ImGui.Spacing(); + + 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); + + 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; + } + + ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f)); + + ImGui.Spacing(); + } + + private void DrawWindowStyleSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_ThemeAndLayout_WindowStyle_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGui.Checkbox(Language.Options_ShowTitleBar_Name, ref Mutable.ShowTitleBar); + + ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar); + + ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton); + ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description); + + ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView); + ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + // Slider 50–100 % UX-Range; intern 0.5–1.0 als WindowOpacity-Float. + // Untere Schwelle 50 % verhindert versehentliches Komplett-Wegblenden + // des Chat-Hintergrunds (war v1.2.0 Bug bei WindowAlpha=0). + var opacityPercent = Mutable.WindowOpacity * 100f; + if (ImGuiUtil.DragFloatVertical( + HellionStrings.Settings_ThemeAndLayout_WindowOpacity_Name, + ref opacityPercent, + .25f, + 50f, + 100f, + $"{opacityPercent:N0}%%", + ImGuiSliderFlags.AlwaysClamp)) + { + Mutable.WindowOpacity = opacityPercent / 100f; + } + ImGuiUtil.HelpMarker(HellionStrings.Settings_ThemeAndLayout_WindowOpacity_Description); + } + } + + private void DrawTimestampStyleSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Settings_ThemeAndLayout_TimestampStyle_Heading); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGui.Checkbox(Language.Options_PrettierTimestamps_Name, ref Mutable.PrettierTimestamps); + ImGuiUtil.HelpMarker(Language.Options_PrettierTimestamps_Description); + + if (Mutable.PrettierTimestamps) + { + ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty); + ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description); + + ImGui.Checkbox(HellionStrings.Appearance_UseCompactDensity_Name, ref Mutable.UseCompactDensity); + ImGuiUtil.HelpMarker(HellionStrings.Appearance_UseCompactDensity_Description); + + ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps); + ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description); + } + + ImGui.Checkbox(Language.Options_Use24HourClock_Name, ref Mutable.Use24HourClock); + ImGuiUtil.HelpMarker(Language.Options_Use24HourClock_Description); + } + } +} diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs deleted file mode 100644 index be66c5d..0000000 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ /dev/null @@ -1,223 +0,0 @@ -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 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(); - } -} diff --git a/HellionChat/Ui/SettingsTabs/Window.cs b/HellionChat/Ui/SettingsTabs/Window.cs index 6372581..561bd24 100644 --- a/HellionChat/Ui/SettingsTabs/Window.cs +++ b/HellionChat/Ui/SettingsTabs/Window.cs @@ -120,7 +120,7 @@ internal sealed class Window : ISettingsTab private void DrawFrameSection() { - using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_Frame_Heading); + using var tree = ImRaii.TreeNode(HellionStrings.Settings_Window_Frame_Behaviour_Heading); if (!tree.Success) { return; @@ -129,23 +129,12 @@ internal sealed class Window : ISettingsTab using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { ImGui.Checkbox(Language.Options_CanMove_Name, ref Mutable.CanMove); - ImGui.Checkbox(Language.Options_CanResize_Name, ref Mutable.CanResize); - ImGui.Checkbox(Language.Options_ShowTitleBar_Name, ref Mutable.ShowTitleBar); - - ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar); - // v0.6.0 — global master switch for the pop-out input bar. ImGui.Checkbox(HellionStrings.Settings_Window_PopOutInputEnabled_Name, ref Mutable.PopOutInputEnabled); ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_PopOutInputEnabled_Description); - ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton); - ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description); - - ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView); - ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)); - ImGui.Spacing(); // Manual escape hatch for off-screen windows. The plugin already From b190456005403ffa06d6f0120a7a19dfd94b5870 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Wed, 6 May 2026 08:46:07 +0200 Subject: [PATCH 3/4] chore: bump version to 1.2.1 and write changelog --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 45 ++++++++++++++++++++++++++++++++++ repo.json | 12 ++++----- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 96cfba1..15377c3 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -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. --> - 1.2.0 + 1.2.1 enable enable