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