dd3a0ea069
HellionStyle.PushGlobal nimmt jetzt eine Theme-Instance + Window-Opacity und liest alle Color- und Style-Slots aus dem aktiven Theme statt aus einer fixen Konstanten-Tabelle. Plugin hält die ThemeRegistry und schaltet beim Init auf das in Config.Theme gespeicherte Slug. Configuration v13 → v14: - Neue Felder Theme (slug), WindowOpacity, ReduceMotion, UseCompactDensity, ShowThemeQuickPicker - HellionThemeEnabled und HellionThemeWindowOpacity sind ab v14 [Obsolete] und bleiben bis v1.2.0 als JSON-Safety-Net erhalten - Migration setzt alle Bestandsuser auf hellion-arctic; chat2-classic bleibt im Themes-Tab als Upstream-Look wählbar - WindowOpacity übernimmt den Wert von HellionThemeWindowOpacity, alte HellionThemeEnabled-Flag entfällt funktional (Theme-Engine ist immer aktiv) Konsumenten der alten Felder (ChatLogWindow.BgAlpha, Popout.BgAlpha) lesen jetzt das neue WindowOpacity. Die Settings-UI in Appearance.cs schreibt übergangsweise weiter in die Obsolete-Felder; Phase J ersetzt diesen Block durch den dedizierten Themes-Tab. CS0612/CS0618 sind dort gezielt mit pragma gekapselt.
850 lines
31 KiB
C#
Executable File
850 lines
31 KiB
C#
Executable File
using System.Collections;
|
||
using HellionChat.Code;
|
||
using HellionChat.GameFunctions.Types;
|
||
using HellionChat.Resources;
|
||
using HellionChat.Util;
|
||
using Dalamud;
|
||
using Dalamud.Configuration;
|
||
using Dalamud.Game.ClientState.Keys;
|
||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||
using Dalamud.Interface.FontIdentifier;
|
||
using Dalamud.Bindings.ImGui;
|
||
|
||
namespace HellionChat;
|
||
|
||
[Serializable]
|
||
public class ConfigKeyBind
|
||
{
|
||
public ModifierFlag Modifier;
|
||
public VirtualKey Key;
|
||
|
||
public override string ToString()
|
||
{
|
||
var modString = "";
|
||
if (Modifier.HasFlag(ModifierFlag.Ctrl))
|
||
modString += Language.Keybind_Modifier_Ctrl + " + ";
|
||
if (Modifier.HasFlag(ModifierFlag.Shift))
|
||
modString += Language.Keybind_Modifier_Shift + " + ";
|
||
if (Modifier.HasFlag(ModifierFlag.Alt))
|
||
modString += Language.Keybind_Modifier_Alt + " + ";
|
||
return modString+Key.GetFancyName();
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
public class Configuration : IPluginConfiguration
|
||
{
|
||
private const int LatestVersion = 14;
|
||
|
||
public int Version { get; set; } = LatestVersion;
|
||
|
||
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
|
||
public string Theme = "hellion-arctic";
|
||
|
||
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
|
||
// HellionThemeWindowOpacity beim Bump v13 → v14.
|
||
public float WindowOpacity = 0.85f;
|
||
|
||
// v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
|
||
// vorab angelegt, damit später keine Migration nötig ist.
|
||
public bool ReduceMotion;
|
||
public bool UseCompactDensity;
|
||
public bool ShowThemeQuickPicker;
|
||
|
||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
||
// Master-switch defaults to true; set false to restore upstream behavior.
|
||
public bool PrivacyFilterEnabled = true;
|
||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
||
// Failsafe for ChatTypes added by future FFXIV patches we don't know about.
|
||
public bool PrivacyPersistUnknownChannels;
|
||
|
||
public bool IsAllowedForStorage(ChatType type)
|
||
{
|
||
if (!PrivacyFilterEnabled)
|
||
return true;
|
||
if (PrivacyPersistChannels.Contains(type))
|
||
return true;
|
||
return PrivacyPersistUnknownChannels;
|
||
}
|
||
|
||
// Hellion Chat — Message retention (GDPR data minimization, time axis).
|
||
// Master switch defaults to false; the plugin will not delete history
|
||
// until the user explicitly opts in.
|
||
public bool RetentionEnabled;
|
||
public int RetentionDefaultDays = 30;
|
||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
||
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
||
|
||
// Hellion Chat first-run wizard — opens once on a fresh install. Existing
|
||
// ChatTwo users skip it because the v6→v7 migration sets the flag.
|
||
public bool FirstRunCompleted;
|
||
|
||
// Hellion Chat global ImGui theme — applied to every plugin window in
|
||
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
||
// can flip this off in the Privacy tab.
|
||
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
|
||
public bool HellionThemeEnabled = true;
|
||
|
||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||
// panes more glass-like so the game shines through. Default 0.5
|
||
// matches the maintainer's daily-driver preference; users who want
|
||
// a less translucent look bump it up in Aussehen → Theme.
|
||
[Obsolete("Replaced by WindowOpacity in v14")]
|
||
public float HellionThemeWindowOpacity = 0.5f;
|
||
|
||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
||
// fresh install gets the Hellion typography out-of-the-box; flip OFF
|
||
// to fall back to the user's chosen system or Dalamud font.
|
||
public bool UseHellionFont = true;
|
||
|
||
// Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
|
||
// /tell spawns a session-only tab dedicated to that conversation
|
||
// partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
|
||
public bool EnableAutoTellTabs = true;
|
||
// Hard cap on simultaneously open auto tell tabs. Range enforced by the
|
||
// settings slider (1–50). LRU drop favors greeted tabs first.
|
||
public int AutoTellTabsLimit = 15;
|
||
// When true the sidebar shows only a thin separator before the temp
|
||
// tabs; when false a section header "Active Tells (n)" is rendered.
|
||
public bool AutoTellTabsCompactDisplay;
|
||
// Number of prior tells to preload from the message store when an
|
||
// auto tell tab is spawned. Range 0–100; 0 disables preload.
|
||
public int AutoTellTabsHistoryPreload = 20;
|
||
// Show the greeter "marked-as-greeted" toggle button next to each
|
||
// temp tab and dim the tab name when set. Off by default because the
|
||
// workflow is specific to club-greeter use cases — most users just
|
||
// want the auto tabs themselves without the extra UI affordance.
|
||
public bool AutoTellTabsShowGreetedToggle;
|
||
|
||
// Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
|
||
// input feature. Set to true once the user dismisses the banner from a
|
||
// pop-out window; never reset after that.
|
||
public bool SeenPopOutInputHint;
|
||
|
||
// Hellion Chat — v0.6.0 master switch for the pop-out input bar.
|
||
// Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
|
||
// are session-only and would force the user to re-enable it for every
|
||
// new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
|
||
// because tester feedback called the manual toggle "umständlich, wirkt
|
||
// unfertig". v11 → v12 migration applies the same flip to existing users.
|
||
public bool PopOutInputEnabled = true;
|
||
|
||
// Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
|
||
// chat-header pop-out toolbar button and reminds about the pop-out
|
||
// input default flip. Set to true once the user dismisses the banner
|
||
// from the main chat window; never reset after that.
|
||
public bool SeenPopOutHeaderHint;
|
||
|
||
// Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
|
||
// sets tab.PopOut = true on every new auto-tell tab so the conversation
|
||
// pops out as its own window directly. Closing the pop-out returns the
|
||
// tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
|
||
// because the existing sidebar workflow is what most users (especially
|
||
// club greeters tracking many parallel tells) expect by default.
|
||
public bool AutoTellTabsOpenAsPopout;
|
||
|
||
public int GetRetentionDays(ChatType type)
|
||
{
|
||
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
|
||
return userOverride;
|
||
if (Privacy.PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDefault))
|
||
return specDefault;
|
||
return RetentionDefaultDays;
|
||
}
|
||
|
||
public bool HideChat = true;
|
||
public bool HideDuringCutscenes = true;
|
||
public bool HideWhenNotLoggedIn = true;
|
||
public bool HideWhenUiHidden = true;
|
||
public bool HideInLoadingScreens;
|
||
public bool HideInBattle;
|
||
public bool HideInNewGamePlusMenu;
|
||
public bool HideWhenInactive;
|
||
public int InactivityHideTimeout = 10;
|
||
public bool InactivityHideActiveDuringBattle = true;
|
||
|
||
[Obsolete("Use InactivityHideChannelsV2 instead")]
|
||
public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];
|
||
|
||
public Dictionary<ChatType, (ChatSource, ChatSource)> InactivityHideChannelsV2 = [];
|
||
public bool InactivityHideExtraChatAll = true;
|
||
public HashSet<Guid> InactivityHideExtraChatChannels = [];
|
||
public bool ShowHideButton = true;
|
||
public bool NativeItemTooltips = true;
|
||
public bool PrettierTimestamps = true;
|
||
public bool MoreCompactPretty;
|
||
public bool HideSameTimestamps;
|
||
public bool ShowNoviceNetwork;
|
||
// Hellion Chat — vertical sidebar tab layout reads better than the
|
||
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
|
||
// greeter typically tracks 5–15 simultaneous conversations). Bestand
|
||
// users keep their saved value untouched — only fresh installs pick
|
||
// up the new default.
|
||
public bool SidebarTabView = true;
|
||
public bool PrintChangelog = true;
|
||
public bool OnlyPreviewIf;
|
||
public int PreviewMinimum = 1;
|
||
public PreviewPosition PreviewPosition = PreviewPosition.Inside;
|
||
public CommandHelpSide CommandHelpSide = CommandHelpSide.None;
|
||
public KeybindMode KeybindMode = KeybindMode.Strict;
|
||
public LanguageOverride LanguageOverride = LanguageOverride.None;
|
||
public bool CanMove = true;
|
||
public bool CanResize = true;
|
||
public bool ShowTitleBar = true;
|
||
public bool ShowPopOutTitleBar = true;
|
||
public bool DatabaseBattleMessages;
|
||
public bool LoadPreviousSession;
|
||
public bool FilterIncludePreviousSessions;
|
||
public bool SortAutoTranslate;
|
||
public bool CollapseDuplicateMessages;
|
||
public bool CollapseKeepUniqueLinks;
|
||
public bool PlaySounds = true;
|
||
public bool KeepInputFocus = true;
|
||
public int MaxLinesToRender = 5_000; // 1-10000
|
||
// Default ON to match a German / European 24h locale. The
|
||
// ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
|
||
// CultureInfo.InvariantCulture so the result is consistent across
|
||
// host locales.
|
||
public bool Use24HourClock = true;
|
||
|
||
public bool ShowEmotes = true;
|
||
public HashSet<string> BlockedEmotes = [];
|
||
|
||
public bool FontsEnabled = true;
|
||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
||
public float FontSizeV2 = 12.75f;
|
||
public float SymbolsFontSizeV2 = 12.75f;
|
||
public SingleFontSpec GlobalFontV2 = new()
|
||
{
|
||
// dalamud only ships KR as regular, which chat2 used previously for global fonts
|
||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
|
||
SizePt = 12.75f,
|
||
};
|
||
public SingleFontSpec JapaneseFontV2 = new()
|
||
{
|
||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium),
|
||
SizePt = 12.75f,
|
||
};
|
||
public bool ItalicEnabled;
|
||
public SingleFontSpec ItalicFontV2 = new()
|
||
{
|
||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
|
||
SizePt = 12.75f,
|
||
};
|
||
|
||
public float TooltipOffset;
|
||
public float WindowAlpha = 100f;
|
||
public Dictionary<ChatType, uint> ChatColours = new();
|
||
public bool ColorSelectedInputChannelButton = true;
|
||
public List<Tab> Tabs = [];
|
||
|
||
public bool OverrideStyle;
|
||
public string? ChosenStyle;
|
||
|
||
public ConfigKeyBind? ChatTabForward;
|
||
public ConfigKeyBind? ChatTabBackward;
|
||
|
||
public void UpdateFrom(Configuration other, bool backToOriginal)
|
||
{
|
||
if (backToOriginal)
|
||
foreach (var tab in Tabs.Where(t => t.PopOut))
|
||
tab.PopOut = false;
|
||
|
||
HideChat = other.HideChat;
|
||
HideDuringCutscenes = other.HideDuringCutscenes;
|
||
HideWhenNotLoggedIn = other.HideWhenNotLoggedIn;
|
||
HideWhenUiHidden = other.HideWhenUiHidden;
|
||
HideInLoadingScreens = other.HideInLoadingScreens;
|
||
HideInBattle = other.HideInBattle;
|
||
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
|
||
HideWhenInactive = other.HideWhenInactive;
|
||
InactivityHideTimeout = other.InactivityHideTimeout;
|
||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
||
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
||
ShowHideButton = other.ShowHideButton;
|
||
NativeItemTooltips = other.NativeItemTooltips;
|
||
PrettierTimestamps = other.PrettierTimestamps;
|
||
MoreCompactPretty = other.MoreCompactPretty;
|
||
HideSameTimestamps = other.HideSameTimestamps;
|
||
ShowNoviceNetwork = other.ShowNoviceNetwork;
|
||
SidebarTabView = other.SidebarTabView;
|
||
PrintChangelog = other.PrintChangelog;
|
||
OnlyPreviewIf = other.OnlyPreviewIf;
|
||
PreviewMinimum = other.PreviewMinimum;
|
||
PreviewPosition = other.PreviewPosition;
|
||
CommandHelpSide = other.CommandHelpSide;
|
||
KeybindMode = other.KeybindMode;
|
||
LanguageOverride = other.LanguageOverride;
|
||
CanMove = other.CanMove;
|
||
CanResize = other.CanResize;
|
||
ShowTitleBar = other.ShowTitleBar;
|
||
ShowPopOutTitleBar = other.ShowPopOutTitleBar;
|
||
DatabaseBattleMessages = other.DatabaseBattleMessages;
|
||
LoadPreviousSession = other.LoadPreviousSession;
|
||
FilterIncludePreviousSessions = other.FilterIncludePreviousSessions;
|
||
SortAutoTranslate = other.SortAutoTranslate;
|
||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||
PlaySounds = other.PlaySounds;
|
||
KeepInputFocus = other.KeepInputFocus;
|
||
MaxLinesToRender = other.MaxLinesToRender;
|
||
Use24HourClock = other.Use24HourClock;
|
||
ShowEmotes = other.ShowEmotes;
|
||
// Deep-copy the set so the live and mutable Configuration instances don't share state
|
||
// — a HashSet reference assignment would cause edits in the settings window to leak
|
||
// into the live config before the user clicks Save.
|
||
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||
FontsEnabled = other.FontsEnabled;
|
||
ItalicEnabled = other.ItalicEnabled;
|
||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||
FontSizeV2 = other.FontSizeV2;
|
||
GlobalFontV2 = other.GlobalFontV2;
|
||
JapaneseFontV2 = other.JapaneseFontV2;
|
||
ItalicFontV2 = other.ItalicFontV2;
|
||
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
||
TooltipOffset = other.TooltipOffset;
|
||
WindowAlpha = other.WindowAlpha;
|
||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||
|
||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
||
// never present in a disk-loaded copy. Keep the live temp tabs of
|
||
// *this* configuration alive across an UpdateFrom so a settings
|
||
// save (or sidebar-mode toggle) does not silently destroy the
|
||
// user's open tell conversations. Persistent tabs from `other`
|
||
// still get the regular clone-replace treatment.
|
||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList();
|
||
Tabs.AddRange(liveTempTabs);
|
||
|
||
OverrideStyle = other.OverrideStyle;
|
||
ChosenStyle = other.ChosenStyle;
|
||
ChatTabForward = other.ChatTabForward;
|
||
ChatTabBackward = other.ChatTabBackward;
|
||
|
||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
||
PrivacyPersistChannels = [..other.PrivacyPersistChannels];
|
||
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
||
|
||
RetentionEnabled = other.RetentionEnabled;
|
||
RetentionDefaultDays = other.RetentionDefaultDays;
|
||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
|
||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||
|
||
FirstRunCompleted = other.FirstRunCompleted;
|
||
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
|
||
HellionThemeEnabled = other.HellionThemeEnabled;
|
||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||
#pragma warning restore CS0612, CS0618
|
||
UseHellionFont = other.UseHellionFont;
|
||
|
||
// v1.1.0 theme engine fields
|
||
Theme = other.Theme;
|
||
WindowOpacity = other.WindowOpacity;
|
||
ReduceMotion = other.ReduceMotion;
|
||
UseCompactDensity = other.UseCompactDensity;
|
||
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
||
|
||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||
|
||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||
PopOutInputEnabled = other.PopOutInputEnabled;
|
||
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
|
||
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
public enum UnreadMode
|
||
{
|
||
All,
|
||
Unseen,
|
||
None,
|
||
}
|
||
|
||
public static class UnreadModeExt
|
||
{
|
||
internal static string Name(this UnreadMode mode) => mode switch
|
||
{
|
||
UnreadMode.All => Language.UnreadMode_All,
|
||
UnreadMode.Unseen => Language.UnreadMode_Unseen,
|
||
UnreadMode.None => Language.UnreadMode_None,
|
||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||
};
|
||
|
||
internal static string? Tooltip(this UnreadMode mode) => mode switch
|
||
{
|
||
UnreadMode.All => Language.UnreadMode_All_Tooltip,
|
||
UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip,
|
||
UnreadMode.None => Language.UnreadMode_None_Tooltip,
|
||
_ => null,
|
||
};
|
||
}
|
||
|
||
[Serializable]
|
||
public class Tab
|
||
{
|
||
public string Name = Language.Tab_DefaultName;
|
||
|
||
[Obsolete("Removed in favor of SelectedChannels")]
|
||
public Dictionary<ChatType, ChatSource> ChatCodes = new();
|
||
|
||
public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
|
||
public bool ExtraChatAll;
|
||
public HashSet<Guid> ExtraChatChannels = [];
|
||
|
||
public UnreadMode UnreadMode = UnreadMode.Unseen;
|
||
public bool UnhideOnActivity;
|
||
public bool DisplayTimestamp = true;
|
||
public InputChannel? Channel;
|
||
public bool PopOut;
|
||
public bool IndependentOpacity;
|
||
public float Opacity = 100f;
|
||
public bool InputDisabled;
|
||
|
||
public bool CanMove = true;
|
||
public bool CanResize = true;
|
||
|
||
public bool IndependentHide;
|
||
public bool HideDuringCutscenes = true;
|
||
public bool HideWhenNotLoggedIn = true;
|
||
public bool HideWhenUiHidden = true;
|
||
public bool HideInLoadingScreens;
|
||
public bool HideInBattle;
|
||
public bool HideWhenInactive;
|
||
|
||
public bool IsTempTab;
|
||
public bool AllSenderMessages;
|
||
public TellTarget TellTarget = TellTarget.Empty();
|
||
|
||
[NonSerialized] public uint Unread;
|
||
[NonSerialized] public uint LastSendUnread;
|
||
[NonSerialized] public long LastActivity;
|
||
[NonSerialized] public MessageList Messages = new();
|
||
|
||
[NonSerialized] public UsedChannel CurrentChannel = new();
|
||
|
||
[NonSerialized] public Guid Identifier = Guid.NewGuid();
|
||
|
||
// Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
|
||
// sidebar to mark a tell partner as already greeted in the current
|
||
// session. NonSerialized because the temp tab itself is session-only.
|
||
[NonSerialized] public bool IsGreeted;
|
||
|
||
public bool Matches(Message message)
|
||
{
|
||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// Auto-tell temp tabs are bound to a single conversation partner;
|
||
// every other tell that matches the channel filter must NOT land
|
||
// here, otherwise all temp tabs would mirror "Tell Exclusive".
|
||
if (IsTempTab && TellTarget?.IsSet() == true)
|
||
{
|
||
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public void AddMessage(Message message, bool unread = true)
|
||
{
|
||
Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||
if (!unread)
|
||
return;
|
||
|
||
Unread += 1;
|
||
if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels))
|
||
LastActivity = Environment.TickCount64;
|
||
}
|
||
|
||
public void Clear()
|
||
=> Messages.Clear();
|
||
|
||
public Tab Clone()
|
||
{
|
||
return new Tab
|
||
{
|
||
Name = Name,
|
||
SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||
ExtraChatAll = ExtraChatAll,
|
||
ExtraChatChannels = ExtraChatChannels.ToHashSet(),
|
||
UnreadMode = UnreadMode,
|
||
UnhideOnActivity = UnhideOnActivity,
|
||
Unread = Unread,
|
||
LastActivity = LastActivity,
|
||
DisplayTimestamp = DisplayTimestamp,
|
||
Channel = Channel,
|
||
PopOut = PopOut,
|
||
IndependentOpacity = IndependentOpacity,
|
||
Opacity = Opacity,
|
||
Identifier = Identifier,
|
||
InputDisabled = InputDisabled,
|
||
CurrentChannel = CurrentChannel,
|
||
CanMove = CanMove,
|
||
CanResize = CanResize,
|
||
IndependentHide = IndependentHide,
|
||
HideDuringCutscenes = HideDuringCutscenes,
|
||
HideWhenNotLoggedIn = HideWhenNotLoggedIn,
|
||
HideWhenUiHidden = HideWhenUiHidden,
|
||
HideInLoadingScreens = HideInLoadingScreens,
|
||
HideInBattle = HideInBattle,
|
||
HideWhenInactive = HideWhenInactive,
|
||
IsTempTab = IsTempTab,
|
||
AllSenderMessages = AllSenderMessages,
|
||
TellTarget = TellTarget.From(TellTarget),
|
||
IsGreeted = IsGreeted,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// MessageList provides an ordered list of messages with duplicate ID
|
||
/// tracking, sorting and mutex protection.
|
||
/// </summary>
|
||
public class MessageList
|
||
{
|
||
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
||
|
||
private readonly List<Message> Messages;
|
||
private readonly HashSet<Guid> TrackedMessageIds;
|
||
|
||
public MessageList()
|
||
{
|
||
Messages = [];
|
||
TrackedMessageIds = [];
|
||
}
|
||
|
||
public MessageList(int initialCapacity)
|
||
{
|
||
Messages = new List<Message>(initialCapacity);
|
||
TrackedMessageIds = new HashSet<Guid>(initialCapacity);
|
||
}
|
||
|
||
public void AddPrune(Message message, int max)
|
||
{
|
||
LockSlim.Wait(-1);
|
||
try
|
||
{
|
||
AddLocked(message);
|
||
PruneMaxLocked(max);
|
||
}
|
||
finally
|
||
{
|
||
LockSlim.Release();
|
||
}
|
||
}
|
||
|
||
public void AddSortPrune(IEnumerable<Message> messages, int max)
|
||
{
|
||
LockSlim.Wait(-1);
|
||
try
|
||
{
|
||
foreach (var message in messages)
|
||
AddLocked(message);
|
||
|
||
SortLocked();
|
||
PruneMaxLocked(max);
|
||
}
|
||
finally
|
||
{
|
||
LockSlim.Release();
|
||
}
|
||
}
|
||
|
||
private void AddLocked(Message message)
|
||
{
|
||
if (TrackedMessageIds.Contains(message.Id))
|
||
return;
|
||
|
||
Messages.Add(message);
|
||
TrackedMessageIds.Add(message.Id);
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
LockSlim.Wait(-1);
|
||
try
|
||
{
|
||
Messages.Clear();
|
||
TrackedMessageIds.Clear();
|
||
}
|
||
finally
|
||
{
|
||
LockSlim.Release();
|
||
}
|
||
}
|
||
|
||
private void SortLocked()
|
||
{
|
||
Messages.Sort((a, b) => a.Date.CompareTo(b.Date));
|
||
}
|
||
|
||
private void PruneMaxLocked(int max)
|
||
{
|
||
while (Messages.Count > max)
|
||
{
|
||
TrackedMessageIds.Remove(Messages[0].Id);
|
||
Messages.RemoveAt(0);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns an array copy of the message list for usage outside of main thread
|
||
/// </summary>
|
||
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
||
{
|
||
await LockSlim.WaitAsync(millisecondsTimeout);
|
||
try
|
||
{
|
||
return Messages.ToArray();
|
||
}
|
||
finally
|
||
{
|
||
LockSlim.Release();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// GetReadOnly returns a read-only list of messages while holding a
|
||
/// reader lock. The list should be used with a using statement.
|
||
/// </summary>
|
||
public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
|
||
{
|
||
LockSlim.Wait(millisecondsTimeout);
|
||
return new RLockedMessageList(LockSlim, Messages);
|
||
}
|
||
|
||
public class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages) : IReadOnlyList<Message>, IDisposable
|
||
{
|
||
public IEnumerator<Message> GetEnumerator()
|
||
{
|
||
return messages.GetEnumerator();
|
||
}
|
||
|
||
IEnumerator IEnumerable.GetEnumerator()
|
||
{
|
||
return GetEnumerator();
|
||
}
|
||
|
||
public int Count => messages.Count;
|
||
|
||
public Message this[int index] => messages[index];
|
||
|
||
public void Dispose()
|
||
{
|
||
lockSlim.Release();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public class UsedChannel
|
||
{
|
||
public InputChannel Channel = InputChannel.Invalid;
|
||
public List<Chunk> Name = [];
|
||
public TellTarget? TellTarget;
|
||
|
||
public bool UseTempChannel;
|
||
public InputChannel TempChannel = InputChannel.Invalid;
|
||
public TellTarget? TempTellTarget;
|
||
|
||
public void ResetTempChannel()
|
||
{
|
||
UseTempChannel = false;
|
||
TempTellTarget = null;
|
||
TempChannel = InputChannel.Invalid;
|
||
}
|
||
|
||
public void SetChannel(InputChannel channel)
|
||
{
|
||
Channel = channel;
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
public enum PreviewPosition
|
||
{
|
||
None,
|
||
Inside,
|
||
Top,
|
||
Bottom,
|
||
Tooltip,
|
||
}
|
||
|
||
public static class PreviewPositionExt
|
||
{
|
||
public static string Name(this PreviewPosition position) => position switch
|
||
{
|
||
PreviewPosition.None => Language.Options_Preview_None,
|
||
PreviewPosition.Inside => Language.Options_Preview_Inside,
|
||
PreviewPosition.Top => Language.Options_Preview_Top,
|
||
PreviewPosition.Bottom => Language.Options_Preview_Bottom,
|
||
PreviewPosition.Tooltip => Language.Options_Preview_Tooltip,
|
||
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null),
|
||
};
|
||
}
|
||
|
||
[Serializable]
|
||
public enum CommandHelpSide
|
||
{
|
||
None,
|
||
Left,
|
||
Right,
|
||
}
|
||
|
||
public static class CommandHelpSideExt
|
||
{
|
||
public static string Name(this CommandHelpSide side) => side switch
|
||
{
|
||
CommandHelpSide.None => Language.CommandHelpSide_None,
|
||
CommandHelpSide.Left => Language.CommandHelpSide_Left,
|
||
CommandHelpSide.Right => Language.CommandHelpSide_Right,
|
||
_ => throw new ArgumentOutOfRangeException(nameof(side), side, null),
|
||
};
|
||
}
|
||
|
||
[Serializable]
|
||
public enum KeybindMode
|
||
{
|
||
Flexible,
|
||
Strict,
|
||
}
|
||
|
||
public static class KeybindModeExt
|
||
{
|
||
public static string Name(this KeybindMode mode) => mode switch
|
||
{
|
||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Name,
|
||
KeybindMode.Strict => Language.KeybindMode_Strict_Name,
|
||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||
};
|
||
|
||
public static string? Tooltip(this KeybindMode mode) => mode switch
|
||
{
|
||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
|
||
KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
|
||
_ => null,
|
||
};
|
||
}
|
||
|
||
[Serializable]
|
||
public enum LanguageOverride
|
||
{
|
||
None,
|
||
ChineseSimplified,
|
||
ChineseTraditional,
|
||
Dutch,
|
||
English,
|
||
French,
|
||
German,
|
||
Greek,
|
||
|
||
// Italian,
|
||
Japanese,
|
||
|
||
// Korean,
|
||
// Norwegian,
|
||
PortugueseBrazil,
|
||
Romanian,
|
||
Russian,
|
||
Spanish,
|
||
Swedish,
|
||
}
|
||
|
||
public static class LanguageOverrideExt
|
||
{
|
||
public static string Name(this LanguageOverride mode) => mode switch
|
||
{
|
||
LanguageOverride.None => Language.LanguageOverride_None,
|
||
LanguageOverride.ChineseSimplified => "简体中文",
|
||
LanguageOverride.ChineseTraditional => "繁體中文",
|
||
LanguageOverride.Dutch => "Nederlands",
|
||
LanguageOverride.English => "English",
|
||
LanguageOverride.French => "Français",
|
||
LanguageOverride.German => "Deutsch",
|
||
LanguageOverride.Greek => "Ελληνικά",
|
||
// LanguageOverride.Italian => "Italiano",
|
||
LanguageOverride.Japanese => "日本語",
|
||
// LanguageOverride.Korean => "한국어 (Korean)",
|
||
// LanguageOverride.Norwegian => "Norsk",
|
||
LanguageOverride.PortugueseBrazil => "Português do Brasil",
|
||
LanguageOverride.Romanian => "Română",
|
||
LanguageOverride.Russian => "Русский",
|
||
LanguageOverride.Spanish => "Español",
|
||
LanguageOverride.Swedish => "Svenska",
|
||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||
};
|
||
|
||
public static string Code(this LanguageOverride mode) => mode switch
|
||
{
|
||
LanguageOverride.None => "",
|
||
LanguageOverride.ChineseSimplified => "zh-hans",
|
||
LanguageOverride.ChineseTraditional => "zh-hant",
|
||
LanguageOverride.Dutch => "nl",
|
||
LanguageOverride.English => "en",
|
||
LanguageOverride.French => "fr",
|
||
LanguageOverride.German => "de",
|
||
LanguageOverride.Greek => "el",
|
||
// LanguageOverride.Italian => "it",
|
||
LanguageOverride.Japanese => "ja",
|
||
// LanguageOverride.Korean => "ko",
|
||
// LanguageOverride.Norwegian => "no",
|
||
LanguageOverride.PortugueseBrazil => "pt-br",
|
||
LanguageOverride.Romanian => "ro",
|
||
LanguageOverride.Russian => "ru",
|
||
LanguageOverride.Spanish => "es",
|
||
LanguageOverride.Swedish => "sv",
|
||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||
};
|
||
}
|
||
|
||
[Serializable]
|
||
[Flags]
|
||
public enum ExtraGlyphRanges
|
||
{
|
||
ChineseFull = 1 << 0,
|
||
ChineseSimplifiedCommon = 1 << 1,
|
||
Cyrillic = 1 << 2,
|
||
Japanese = 1 << 3,
|
||
Korean = 1 << 4,
|
||
Thai = 1 << 5,
|
||
Vietnamese = 1 << 6,
|
||
}
|
||
|
||
public static class ExtraGlyphRangesExt
|
||
{
|
||
public static string Name(this ExtraGlyphRanges ranges) => ranges switch
|
||
{
|
||
ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name,
|
||
ExtraGlyphRanges.ChineseSimplifiedCommon => Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name,
|
||
ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name,
|
||
ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name,
|
||
ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
|
||
ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name,
|
||
ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name,
|
||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||
};
|
||
|
||
public static unsafe nint Range(this ExtraGlyphRanges ranges) => ranges switch
|
||
{
|
||
ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(),
|
||
ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(),
|
||
ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(),
|
||
ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(),
|
||
ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
|
||
ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
|
||
ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
|
||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||
};
|
||
}
|