refactor: re-sort settings cards thematically for v1.2.1
- 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
This commit is contained in:
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 15;
|
private const int LatestVersion = 16;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -49,7 +49,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
// vorab angelegt, damit später keine Migration nötig ist.
|
// vorab angelegt, damit später keine Migration nötig ist.
|
||||||
public bool ReduceMotion;
|
public bool ReduceMotion;
|
||||||
public bool UseCompactDensity;
|
public bool UseCompactDensity;
|
||||||
public bool ShowThemeQuickPicker;
|
|
||||||
|
|
||||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
||||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
// Master-switch defaults to true; set false to restore upstream behavior.
|
||||||
@@ -222,14 +221,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
};
|
};
|
||||||
|
|
||||||
public float TooltipOffset;
|
public float TooltipOffset;
|
||||||
public float WindowAlpha = 100f;
|
|
||||||
public Dictionary<ChatType, uint> ChatColours = new();
|
public Dictionary<ChatType, uint> ChatColours = new();
|
||||||
public bool ColorSelectedInputChannelButton = true;
|
public bool ColorSelectedInputChannelButton = true;
|
||||||
public List<Tab> Tabs = [];
|
public List<Tab> Tabs = [];
|
||||||
|
|
||||||
public bool OverrideStyle;
|
|
||||||
public string? ChosenStyle;
|
|
||||||
|
|
||||||
public ConfigKeyBind? ChatTabForward;
|
public ConfigKeyBind? ChatTabForward;
|
||||||
public ConfigKeyBind? ChatTabBackward;
|
public ConfigKeyBind? ChatTabBackward;
|
||||||
|
|
||||||
@@ -294,7 +289,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
ItalicFontV2 = other.ItalicFontV2;
|
ItalicFontV2 = other.ItalicFontV2;
|
||||||
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
||||||
TooltipOffset = other.TooltipOffset;
|
TooltipOffset = other.TooltipOffset;
|
||||||
WindowAlpha = other.WindowAlpha;
|
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
@@ -331,8 +325,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
Tabs.AddRange(liveTempTabs);
|
Tabs.AddRange(liveTempTabs);
|
||||||
|
|
||||||
OverrideStyle = other.OverrideStyle;
|
|
||||||
ChosenStyle = other.ChosenStyle;
|
|
||||||
ChatTabForward = other.ChatTabForward;
|
ChatTabForward = other.ChatTabForward;
|
||||||
ChatTabBackward = other.ChatTabBackward;
|
ChatTabBackward = other.ChatTabBackward;
|
||||||
|
|
||||||
@@ -353,7 +345,6 @@ public class Configuration : IPluginConfiguration
|
|||||||
WindowOpacity = other.WindowOpacity;
|
WindowOpacity = other.WindowOpacity;
|
||||||
ReduceMotion = other.ReduceMotion;
|
ReduceMotion = other.ReduceMotion;
|
||||||
UseCompactDensity = other.UseCompactDensity;
|
UseCompactDensity = other.UseCompactDensity;
|
||||||
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
|
||||||
|
|
||||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||||
|
|||||||
+94
-1
@@ -251,7 +251,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
// User die direkt v13 → v15 springen bekommen den Default 0.85.
|
// User die direkt v13 → v15 springen bekommen den Default 0.85.
|
||||||
Config.ReduceMotion = false;
|
Config.ReduceMotion = false;
|
||||||
Config.UseCompactDensity = false;
|
Config.UseCompactDensity = false;
|
||||||
Config.ShowThemeQuickPicker = false;
|
|
||||||
Config.Version = 14;
|
Config.Version = 14;
|
||||||
SaveConfig();
|
SaveConfig();
|
||||||
Log.Information(
|
Log.Information(
|
||||||
@@ -273,6 +272,100 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
|
"(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
|
// Hellion v1.0.0 default tab layout. Five thematically separated
|
||||||
// tabs: General catches the immediate-surroundings public chat
|
// tabs: General catches the immediate-surroundings public chat
|
||||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new General(Plugin, Mutable),
|
new General(Plugin, Mutable),
|
||||||
new Appearance(Plugin, Mutable),
|
new ThemeAndLayout(Plugin, Mutable),
|
||||||
new SettingsTabs.Themes(Plugin, Mutable),
|
new FontsAndColours(Plugin, Mutable),
|
||||||
new SettingsTabs.Window(Plugin, Mutable),
|
new SettingsTabs.Window(Plugin, Mutable),
|
||||||
new Chat(Plugin, Mutable),
|
new Chat(Plugin, Mutable),
|
||||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||||
new SettingsTabs.Privacy(Plugin, Mutable),
|
new SettingsTabs.Privacy(Plugin, Mutable),
|
||||||
new Database(Plugin, Mutable),
|
new DataManagement(Plugin, Mutable),
|
||||||
new Information(Mutable),
|
new Information(Mutable),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,18 +12,21 @@ internal sealed class SettingsOverview
|
|||||||
private readonly SettingsWindow _window;
|
private readonly SettingsWindow _window;
|
||||||
|
|
||||||
// Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow.
|
// 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 =
|
private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs =
|
||||||
[
|
[
|
||||||
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
||||||
(FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"),
|
(FontAwesomeIcon.Palette, "Settings_Card_ThemeAndLayout_Title", "Settings_Card_ThemeAndLayout_Subtext"),
|
||||||
(FontAwesomeIcon.Swatchbook, "Settings_Card_Themes_Title", "Settings_Card_Themes_Subtext"),
|
(FontAwesomeIcon.Font, "Settings_Card_FontsAndColours_Title", "Settings_Card_FontsAndColours_Subtext"),
|
||||||
(FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"),
|
(FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"),
|
||||||
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
||||||
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
||||||
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
||||||
(FontAwesomeIcon.Database, "Settings_Card_Database_Title", "Settings_Card_Database_Subtext"),
|
(FontAwesomeIcon.Database, "Settings_Card_DataManagement_Title", "Settings_Card_DataManagement_Subtext"),
|
||||||
(FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"),
|
(FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"),
|
||||||
];
|
];
|
||||||
|
|
||||||
public SettingsOverview(SettingsWindow window)
|
public SettingsOverview(SettingsWindow window)
|
||||||
|
|||||||
@@ -89,6 +89,21 @@ internal sealed class Chat : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<int, long>? CleanupCounts;
|
||||||
|
private long CleanupKeepCount;
|
||||||
|
private long CleanupDeleteCount;
|
||||||
|
private bool CleanupRunning;
|
||||||
|
private bool CleanupPreviewStale;
|
||||||
|
private HashSet<ChatType>? 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<ChatType> 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<string> 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<ChatType>(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<Message>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Message>(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-115
@@ -4,21 +4,20 @@ using HellionChat.Util;
|
|||||||
using Dalamud;
|
using Dalamud;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
using Dalamud.Interface.Style;
|
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
internal sealed class Appearance : ISettingsTab
|
internal sealed class FontsAndColours : ISettingsTab
|
||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { 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;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
@@ -26,94 +25,27 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
{
|
{
|
||||||
DrawThemeSection();
|
|
||||||
ImGui.Spacing();
|
|
||||||
DrawFontsSection();
|
DrawFontsSection();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
DrawColoursSection();
|
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()
|
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)
|
if (!tree.Success)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
{
|
{
|
||||||
if (ImGui.Checkbox(HellionStrings.Theme_UseHellionFont_Name, ref Mutable.UseHellionFont))
|
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)
|
if (Mutable.UseHellionFont)
|
||||||
Mutable.FontsEnabled = false;
|
Mutable.FontsEnabled = false;
|
||||||
}
|
}
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.Theme_UseHellionFont_Description);
|
||||||
ImGui.Spacing();
|
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)
|
if (Mutable.UseHellionFont)
|
||||||
{
|
{
|
||||||
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
|
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
|
||||||
@@ -154,7 +86,6 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
ImGuiUtil.WarningText(Language.Options_Font_Warning);
|
ImGuiUtil.WarningText(Language.Options_Font_Warning);
|
||||||
ImGui.Spacing();
|
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, "いろはにほへと ちりぬるを");
|
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref unused, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
|
||||||
japaneseChooser?.ResultTask.ContinueWith(r =>
|
japaneseChooser?.ResultTask.ContinueWith(r =>
|
||||||
{
|
{
|
||||||
@@ -215,11 +146,9 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
|
|
||||||
private void DrawColoursSection()
|
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)
|
if (!tree.Success)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
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()
|
private void DrawColourPresetButtons()
|
||||||
{
|
{
|
||||||
var first = true;
|
var first = true;
|
||||||
@@ -282,7 +208,6 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
|
|
||||||
if (preset.IsBrandPreset)
|
if (preset.IsBrandPreset)
|
||||||
{
|
{
|
||||||
// Hellion-Brand visuell hervorheben — blau-violetter Frame-Akzent
|
|
||||||
var border = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(255, 128, 200));
|
var border = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(255, 128, 200));
|
||||||
var btn = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(74, 42, 106));
|
var btn = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(74, 42, 106));
|
||||||
ImGui.PushStyleColor(ImGuiCol.Border, new System.Numerics.Vector4(border.X, border.Y, border.Z, 1f));
|
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)
|
private static string GetPresetLabel(ChatColourPreset preset)
|
||||||
{
|
{
|
||||||
var localized = HellionStrings.ResourceManager.GetString(preset.LocalizationKey, HellionStrings.Culture);
|
var localized = HellionStrings.ResourceManager.GetString(preset.LocalizationKey, HellionStrings.Culture);
|
||||||
@@ -322,36 +244,4 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
GlobalParametersCache.Refresh();
|
GlobalParametersCache.Refresh();
|
||||||
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,28 @@ internal sealed class General : ISettingsTab
|
|||||||
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
|
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
|
||||||
ImGui.SetNextItemWidth(-1);
|
ImGui.SetNextItemWidth(-1);
|
||||||
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
|
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<KeybindMode>())
|
||||||
|
{
|
||||||
|
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));
|
ImGuiUtil.HelpMarker(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
|
|
||||||
{
|
|
||||||
if (combo.Success)
|
|
||||||
{
|
|
||||||
foreach (var mode in Enum.GetValues<KeybindMode>())
|
|
||||||
{
|
|
||||||
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);
|
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_SortAutoTranslate_Description);
|
ImGuiUtil.HelpMarker(Language.Options_SortAutoTranslate_Description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Export;
|
|
||||||
using HellionChat.Privacy;
|
using HellionChat.Privacy;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Dalamud.Interface.Colors;
|
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
@@ -52,25 +48,6 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
private Dictionary<int, long>? CleanupCounts;
|
|
||||||
private long CleanupKeepCount;
|
|
||||||
private long CleanupDeleteCount;
|
|
||||||
private bool CleanupRunning;
|
|
||||||
private bool CleanupPreviewStale;
|
|
||||||
private HashSet<ChatType>? 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<ChatType> ExportSelectedChannels = [];
|
|
||||||
private ExportFormat ExportFormat = ExportFormat.Markdown;
|
|
||||||
private bool ExportRunning;
|
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
{
|
{
|
||||||
if (ImGui.Button(HellionStrings.Wizard_Reopen_Button))
|
if (ImGui.Button(HellionStrings.Wizard_Reopen_Button))
|
||||||
@@ -78,51 +55,6 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
DrawPrivacyFilterSection();
|
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()
|
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<ChatType>(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Theme> themes, string activeSlug)
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
var columns = avail.X >= 700f ? 3 : 2;
|
||||||
|
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
||||||
|
var cardHeight = 140f;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Theme> themes, string activeSlug)
|
|
||||||
{
|
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
|
||||||
var columns = avail.X >= 700f ? 3 : 2;
|
|
||||||
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
|
||||||
var cardHeight = 140f; // Mockup + Name + Author brauchen den Platz
|
|
||||||
|
|
||||||
var list = themes.ToList();
|
|
||||||
for (var i = 0; i < list.Count; i++)
|
|
||||||
{
|
|
||||||
DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight);
|
|
||||||
|
|
||||||
// SameLine zwischen den Cards einer Reihe; am Spalten-Ende kein
|
|
||||||
// SameLine, dann beginnt automatisch eine neue Zeile.
|
|
||||||
if ((i + 1) % columns != 0 && i != list.Count - 1)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawThemeCard(Theme theme, string activeSlug, float w, float h)
|
|
||||||
{
|
|
||||||
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
|
||||||
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
|
||||||
// einzelnen InvisibleButton-Items separat und das Wrapping bricht.
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
|
|
||||||
var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase);
|
|
||||||
var cursorBefore = ImGui.GetCursorScreenPos();
|
|
||||||
var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h));
|
|
||||||
var hovered = ImGui.IsItemHovered();
|
|
||||||
|
|
||||||
var draw = ImGui.GetWindowDrawList();
|
|
||||||
var bg = ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu);
|
|
||||||
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bg, 4f);
|
|
||||||
|
|
||||||
if (isActive)
|
|
||||||
{
|
|
||||||
var border = ColourUtil.RgbaToAbgr(theme.Colors.Primary);
|
|
||||||
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 2f);
|
|
||||||
}
|
|
||||||
else if (hovered)
|
|
||||||
{
|
|
||||||
var border = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u);
|
|
||||||
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mini-Mockup oben — DrawList-Operation, kein Cursor-Hopping
|
|
||||||
var mockupOrigin = cursorBefore + new Vector2(12f, 12f);
|
|
||||||
var mockupSize = new Vector2(w - 24f, 60f);
|
|
||||||
ThemeMockup.Draw(mockupOrigin, mockupSize, theme);
|
|
||||||
|
|
||||||
// Name + Author direkt via DrawList (statt SetCursorScreenPos +
|
|
||||||
// TextUnformatted), damit der ImGui-Layout-Cursor stabil bleibt
|
|
||||||
// und die BeginGroup/EndGroup-Klammer den Card-Bereich als ein
|
|
||||||
// Layout-Item führt.
|
|
||||||
var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
|
||||||
var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
|
|
||||||
draw.AddText(cursorBefore + new Vector2(12f, 80f), textColor, theme.Name);
|
|
||||||
draw.AddText(cursorBefore + new Vector2(12f, 100f), mutedColor, theme.Author);
|
|
||||||
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
if (clicked)
|
|
||||||
{
|
|
||||||
Mutable.Theme = theme.Slug;
|
|
||||||
Plugin.ThemeRegistry.Switch(theme.Slug);
|
|
||||||
_applyDismissedFor = null; // Banner für neues Theme wieder zeigen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawChatColorsApplyBanner(Theme active)
|
|
||||||
{
|
|
||||||
// Klassik hat per Design keine ChatColors — kein Banner.
|
|
||||||
if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// User hat die Frage bereits für genau dieses Theme beantwortet.
|
|
||||||
if (_applyDismissedFor == active.Slug)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Wenn die aktuellen Channel-Colors bereits exakt mit dem Theme-Vorschlag
|
|
||||||
// übereinstimmen, gibt's nichts zu tun.
|
|
||||||
var alreadyMatching = themeChatColors.Channels.All(kvp =>
|
|
||||||
Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value);
|
|
||||||
if (alreadyMatching)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
// Dezent-Akzent-Banner mit Border in Theme-Primary
|
|
||||||
var border = ColourUtil.RgbaToAbgr(active.Colors.Primary);
|
|
||||||
var bgFill = ColourUtil.RgbaToAbgr((active.Colors.Surface & 0xFFFFFF00u) | 0xCCu);
|
|
||||||
var origin = ImGui.GetCursorScreenPos();
|
|
||||||
var width = ImGui.GetContentRegionAvail().X;
|
|
||||||
var height = 64f;
|
|
||||||
var draw = ImGui.GetWindowDrawList();
|
|
||||||
draw.AddRectFilled(origin, origin + new Vector2(width, height), bgFill, 4f);
|
|
||||||
draw.AddRect(origin, origin + new Vector2(width, height), border, 4f, ImDrawFlags.None, 1f);
|
|
||||||
|
|
||||||
var hint = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Hint")
|
|
||||||
?? "This theme suggests its own chat channel colours.";
|
|
||||||
var applyLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Apply")
|
|
||||||
?? "Apply";
|
|
||||||
var keepLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Keep")
|
|
||||||
?? "Keep current";
|
|
||||||
|
|
||||||
var textColor = ColourUtil.RgbaToAbgr(active.Colors.TextPrimary);
|
|
||||||
draw.AddText(origin + new Vector2(12f, 10f), textColor, hint);
|
|
||||||
|
|
||||||
// Buttons als InvisibleButton + DrawList-Overlay, damit sie konsistent
|
|
||||||
// zum Banner-Look bleiben.
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Button, active.Colors.Primary))
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, active.Colors.PrimaryLight))
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, active.Colors.PrimaryDark))
|
|
||||||
{
|
|
||||||
ImGui.SetCursorScreenPos(origin + new Vector2(12f, 32f));
|
|
||||||
if (ImGui.Button(applyLabel))
|
|
||||||
{
|
|
||||||
foreach (var kvp in themeChatColors.Channels)
|
|
||||||
Mutable.ChatColours[kvp.Key] = kvp.Value;
|
|
||||||
_applyDismissedFor = active.Slug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button(keepLabel))
|
|
||||||
{
|
|
||||||
_applyDismissedFor = active.Slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor unter den Banner setzen
|
|
||||||
ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f));
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -120,7 +120,7 @@ internal sealed class Window : ISettingsTab
|
|||||||
|
|
||||||
private void DrawFrameSection()
|
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)
|
if (!tree.Success)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -129,23 +129,12 @@ internal sealed class Window : ISettingsTab
|
|||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
{
|
{
|
||||||
ImGui.Checkbox(Language.Options_CanMove_Name, ref Mutable.CanMove);
|
ImGui.Checkbox(Language.Options_CanMove_Name, ref Mutable.CanMove);
|
||||||
|
|
||||||
ImGui.Checkbox(Language.Options_CanResize_Name, ref Mutable.CanResize);
|
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.
|
// v0.6.0 — global master switch for the pop-out input bar.
|
||||||
ImGui.Checkbox(HellionStrings.Settings_Window_PopOutInputEnabled_Name, ref Mutable.PopOutInputEnabled);
|
ImGui.Checkbox(HellionStrings.Settings_Window_PopOutInputEnabled_Name, ref Mutable.PopOutInputEnabled);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_PopOutInputEnabled_Description);
|
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();
|
ImGui.Spacing();
|
||||||
|
|
||||||
// Manual escape hatch for off-screen windows. The plugin already
|
// Manual escape hatch for off-screen windows. The plugin already
|
||||||
|
|||||||
Reference in New Issue
Block a user