691 lines
26 KiB
C#
691 lines
26 KiB
C#
using System.Globalization;
|
|
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Interface.Windowing;
|
|
using HellionChat.Branding;
|
|
using HellionChat.Code;
|
|
using HellionChat.Privacy;
|
|
using HellionChat.Resources;
|
|
using HellionChat.Themes;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui;
|
|
|
|
// Multi-step first-run wizard. public sealed because Plugin.cs has a
|
|
// public-typed property on this class — narrowing to internal would
|
|
// be a build break across the assembly boundary. State lives in a
|
|
// nested WizardState record; every step writes nullable Pending*
|
|
// fields, and CommitPending() applies only the non-null ones so
|
|
// users who skip a step never get their existing config overwritten.
|
|
public sealed class FirstRunWizard : Window
|
|
{
|
|
// Forge-Bronze (#C2410C). The same constant lives in ThemeRegistry
|
|
// and the forge-announce workflow; pinning it locally keeps the
|
|
// wizard render path free of registry lookups during draw.
|
|
private static readonly Vector4 ForgeBronze = new(0xC2 / 255f, 0x41 / 255f, 0x0C / 255f, 1f);
|
|
private static readonly Vector4 ForgeBronzeDim = new(
|
|
0xC2 / 255f,
|
|
0x41 / 255f,
|
|
0x0C / 255f,
|
|
0.3f
|
|
);
|
|
|
|
private const int TotalSteps = 4;
|
|
|
|
private readonly Plugin Plugin;
|
|
private readonly WizardState _state = new();
|
|
|
|
internal FirstRunWizard(Plugin plugin)
|
|
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
|
{
|
|
Plugin = plugin;
|
|
|
|
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
|
SizeCondition = ImGuiCond.Appearing;
|
|
Size = new Vector2(720, 480);
|
|
SizeConstraints = new WindowSizeConstraints
|
|
{
|
|
MinimumSize = new Vector2(600, 400),
|
|
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
|
};
|
|
}
|
|
|
|
public override void OnClose()
|
|
{
|
|
// OnClose fires on explicit X-click and on plugin dispose. We never
|
|
// implicitly accept the defaults here — both the explicit "Decide
|
|
// later" footer link and a successful "Finish ✓" set FirstRunCompleted
|
|
// = true, so the wizard does not reopen on the next plugin load
|
|
// regardless of which path the user took.
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
DrawPagination();
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
switch (_state.CurrentStep)
|
|
{
|
|
case 1:
|
|
DrawStepWelcome();
|
|
break;
|
|
case 2:
|
|
DrawStepPrivacy();
|
|
break;
|
|
case 3:
|
|
DrawStepPowerSettings();
|
|
break;
|
|
case 4:
|
|
DrawStepDone();
|
|
break;
|
|
default:
|
|
_state.CurrentStep = 1;
|
|
DrawStepWelcome();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void DrawPagination()
|
|
{
|
|
var draw = ImGui.GetWindowDrawList();
|
|
var avail = ImGui.GetContentRegionAvail();
|
|
var cursor = ImGui.GetCursorScreenPos();
|
|
const float radius = 5f;
|
|
const float spacing = 16f;
|
|
var totalWidth = (TotalSteps - 1) * spacing;
|
|
var startX = cursor.X + avail.X - totalWidth - radius;
|
|
|
|
for (var i = 0; i < TotalSteps; i++)
|
|
{
|
|
var color = (i + 1) == _state.CurrentStep ? ForgeBronze : ForgeBronzeDim;
|
|
var packed = ImGui.GetColorU32(color);
|
|
draw.AddCircleFilled(
|
|
new Vector2(startX + i * spacing, cursor.Y + radius),
|
|
radius,
|
|
packed
|
|
);
|
|
}
|
|
|
|
// Reserve vertical space the circles consumed so the next widget starts below them.
|
|
ImGui.Dummy(new Vector2(0, radius * 2));
|
|
}
|
|
|
|
private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary)
|
|
{
|
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
|
var primaryWidth =
|
|
ImGui.CalcTextSize(primaryLabel).X + ImGui.GetStyle().FramePadding.X * 2 + 16f;
|
|
var avail = ImGui.GetContentRegionAvail();
|
|
|
|
// Push the footer to the bottom of the window so step contents
|
|
// above can size themselves with GetContentRegionAvail().
|
|
var lineHeight = ImGui.GetFrameHeightWithSpacing();
|
|
var pushDown = avail.Y - lineHeight - spacing;
|
|
if (pushDown > 0)
|
|
ImGui.Dummy(new Vector2(0, pushDown));
|
|
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
if (showBack)
|
|
{
|
|
if (ImGui.Button(HellionStrings.Wizard_Nav_Back))
|
|
_state.CurrentStep = Math.Max(1, _state.CurrentStep - 1);
|
|
ImGui.SameLine();
|
|
}
|
|
|
|
if (showSkip)
|
|
{
|
|
if (ImGui.Button(HellionStrings.Wizard_Step1_Skip_Label))
|
|
{
|
|
// Skip path = matches today's Cancel path: mark first-run
|
|
// complete, save, close. No CommitPending — the user said
|
|
// 'decide later', so existing config stays as-is.
|
|
Plugin.Config.FirstRunCompleted = true;
|
|
Plugin.SaveConfig();
|
|
IsOpen = false;
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
ImGuiUtil.Tooltip(HellionStrings.Wizard_Step1_Skip_Tooltip);
|
|
}
|
|
|
|
// Right-align the primary action button.
|
|
var rightX = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X - primaryWidth;
|
|
if (rightX > ImGui.GetCursorPosX())
|
|
ImGui.SameLine(rightX);
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, ForgeBronze))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ForgeBronze))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ForgeBronze))
|
|
{
|
|
if (ImGui.Button($"{primaryLabel}##wizard-primary"))
|
|
onPrimary();
|
|
}
|
|
}
|
|
|
|
private void DrawStepWelcome()
|
|
{
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
|
|
ImGui.Spacing();
|
|
|
|
// Fox-banner image: the embedded Hellion Forge fox artwork. The card
|
|
// behind the image gives the dark fox enough contrast against the
|
|
// plugin's dark UI so the logo reads clearly at a glance.
|
|
var banner = FoxBannerTexture.Shared.GetWrapOrDefault();
|
|
if (banner is not null)
|
|
{
|
|
const uint CardColor = 0xFFE8E8E8; // off-white fill so the dark fox pops
|
|
var imgHeight = 170f * ImGuiHelpers.GlobalScale;
|
|
var imgWidth = imgHeight * banner.Size.X / banner.Size.Y;
|
|
var pad = 14f * ImGuiHelpers.GlobalScale;
|
|
var cardWidth = imgWidth + pad * 2f;
|
|
var cardHeight = imgHeight + pad * 2f;
|
|
var rounding = 8f * ImGuiHelpers.GlobalScale;
|
|
|
|
// Centre the card in the content region. Clamp to zero so the card
|
|
// never shifts left of the window edge on very narrow windows.
|
|
var offsetX = Math.Max(0f, (ImGui.GetContentRegionAvail().X - cardWidth) * 0.5f);
|
|
var cardOrigin = ImGui.GetCursorScreenPos() + new Vector2(offsetX, 0f);
|
|
|
|
// Draw the rounded card behind the image, then place the image on top.
|
|
ImGui
|
|
.GetWindowDrawList()
|
|
.AddRectFilled(
|
|
cardOrigin,
|
|
cardOrigin + new Vector2(cardWidth, cardHeight),
|
|
CardColor,
|
|
rounding
|
|
);
|
|
ImGui.SetCursorScreenPos(cardOrigin + new Vector2(pad, pad));
|
|
ImGui.Image(banner.Handle, new Vector2(imgWidth, imgHeight));
|
|
|
|
// Advance the layout cursor past the full card so the content below
|
|
// starts at the right position and does not overlap the card.
|
|
ImGui.SetCursorScreenPos(cardOrigin);
|
|
ImGui.Dummy(new Vector2(cardWidth, cardHeight));
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
ImGui.TextWrapped(HellionStrings.Wizard_Step1_Subtitle);
|
|
ImGui.Spacing();
|
|
ImGui.TextWrapped(HellionStrings.Wizard_Step1_Footer_Hint);
|
|
|
|
DrawFooter(
|
|
showBack: false,
|
|
showSkip: true,
|
|
HellionStrings.Wizard_Nav_Next,
|
|
() => _state.CurrentStep = 2
|
|
);
|
|
}
|
|
|
|
private void DrawStepPrivacy()
|
|
{
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step2_Title);
|
|
ImGui.Spacing();
|
|
|
|
// Reserve footer height (separator + spacing + button row) so the
|
|
// 2x2 grid uses the rest of the window.
|
|
var footerReserve =
|
|
ImGui.GetFrameHeightWithSpacing()
|
|
+ ImGui.GetStyle().ItemSpacing.Y * 3
|
|
+ ImGui.GetTextLineHeight();
|
|
var grid = ImGui.GetContentRegionAvail();
|
|
var cardWidth = (grid.X - ImGui.GetStyle().ItemSpacing.X) / 2f;
|
|
var cardHeight = (grid.Y - footerReserve - ImGui.GetStyle().ItemSpacing.Y) / 2f;
|
|
|
|
// Top row.
|
|
DrawProfileCard(
|
|
PrivacyProfile.PrivacyFirst,
|
|
"🔒",
|
|
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
|
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
|
|
recommended: false,
|
|
cardWidth,
|
|
cardHeight
|
|
);
|
|
ImGui.SameLine();
|
|
DrawProfileCard(
|
|
PrivacyProfile.Casual,
|
|
"💬",
|
|
HellionStrings.Wizard_Profile_Casual_Heading,
|
|
HellionStrings.Wizard_Profile_Casual_Description,
|
|
recommended: true,
|
|
cardWidth,
|
|
cardHeight
|
|
);
|
|
|
|
// Bottom row.
|
|
DrawProfileCard(
|
|
PrivacyProfile.Roleplay,
|
|
"🎭",
|
|
HellionStrings.Wizard_Profile_Roleplay_Heading,
|
|
HellionStrings.Wizard_Profile_Roleplay_Description,
|
|
recommended: false,
|
|
cardWidth,
|
|
cardHeight
|
|
);
|
|
ImGui.SameLine();
|
|
DrawProfileCard(
|
|
PrivacyProfile.FullHistory,
|
|
"📚",
|
|
HellionStrings.Wizard_Profile_FullHistory_Heading,
|
|
HellionStrings.Wizard_Profile_FullHistory_Description,
|
|
recommended: false,
|
|
cardWidth,
|
|
cardHeight
|
|
);
|
|
|
|
ImGui.Spacing();
|
|
ImGui.TextDisabled(HellionStrings.Wizard_Step2_RecommendedFooter);
|
|
|
|
DrawFooter(
|
|
showBack: true,
|
|
showSkip: true,
|
|
HellionStrings.Wizard_Nav_Next,
|
|
() => _state.CurrentStep = 3
|
|
);
|
|
}
|
|
|
|
private void DrawProfileCard(
|
|
PrivacyProfile profile,
|
|
string emoji,
|
|
string heading,
|
|
string description,
|
|
bool recommended,
|
|
float width,
|
|
float height
|
|
)
|
|
{
|
|
var isSelected = _state.PendingProfile == profile;
|
|
// GetStyleColorVec4 returns a pointer to the live style entry in
|
|
// Dalamud.Bindings.ImGui, which would require unsafe. Use the U32
|
|
// packed-colour overload of PushColor for the default branch so we
|
|
// can stay in safe code while still matching the current border.
|
|
var borderColor = isSelected
|
|
? ImGui.GetColorU32(ForgeBronze)
|
|
: ImGui.GetColorU32(ImGuiCol.Border);
|
|
|
|
using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor);
|
|
using var child = ImRaii.Child(
|
|
$"##profile-card-{profile}",
|
|
new Vector2(width, height),
|
|
true
|
|
);
|
|
if (!child.Success)
|
|
return;
|
|
|
|
// InvisibleButton over the full card area, then SetCursorScreenPos
|
|
// back to draw the heading/description content on top. Selectable
|
|
// would be semantically wrong here — the card is a standalone
|
|
// choice tile, not a list-item inside a list/menu. The button
|
|
// takes the click for the entire card area, and IsItemHovered()
|
|
// on it (if we wire one up later) would naturally cover the full
|
|
// tile. Visual feedback comes from the border colour above.
|
|
var startPos = ImGui.GetCursorScreenPos();
|
|
var cardArea = ImGui.GetContentRegionAvail();
|
|
if (ImGui.InvisibleButton($"##profile-hit-{profile}", cardArea))
|
|
_state.PendingProfile = profile;
|
|
|
|
ImGui.SetCursorScreenPos(startPos);
|
|
|
|
ImGui.TextUnformatted($"{emoji} {heading}{(recommended ? " ★" : string.Empty)}");
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
ImGui.TextWrapped(description);
|
|
}
|
|
|
|
private void DrawStepPowerSettings()
|
|
{
|
|
// Seed only the two recommendation fields here. Other fields remain
|
|
// null until the user touches the corresponding control.
|
|
// Spec FR-4: the wizard explicitly recommends LoadPreviousSession =
|
|
// true and FilterIncludePreviousSessions = true (Config defaults are
|
|
// false). The other four fields (AutoTellTabsHistoryPreload,
|
|
// UseCompactDensity, PrettierTimestamps, Theme) follow the generic
|
|
// null-semantics from Spec Z.176: a null pending means the user did
|
|
// not touch that control, so CommitPending must not write back. They
|
|
// are read live from Plugin.Config below for the ImGui ref-binding
|
|
// but never seeded into Pending* without a user gesture.
|
|
_state.PendingLoadPreviousSession ??= true;
|
|
_state.PendingFilterIncludePreviousSessions ??= true;
|
|
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Title);
|
|
ImGui.Spacing();
|
|
|
|
// History section.
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_History);
|
|
|
|
var loadPrev = _state.PendingLoadPreviousSession ?? true;
|
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_LoadPreviousSession_Label, ref loadPrev))
|
|
{
|
|
_state.PendingLoadPreviousSession = loadPrev;
|
|
// Mirror the DataAndPrivacy coupling: turning load-previous on
|
|
// also turns filter-include on (otherwise old messages bypass
|
|
// the filter chain), and turning filter-include off forces
|
|
// load-previous off. Same idiom as Ui/SettingsTabs/DataAndPrivacy.cs.
|
|
if (loadPrev)
|
|
_state.PendingFilterIncludePreviousSessions = true;
|
|
}
|
|
|
|
var filterPrev = _state.PendingFilterIncludePreviousSessions ?? true;
|
|
if (
|
|
ImGui.Checkbox(
|
|
HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label,
|
|
ref filterPrev
|
|
)
|
|
)
|
|
{
|
|
_state.PendingFilterIncludePreviousSessions = filterPrev;
|
|
if (!filterPrev)
|
|
_state.PendingLoadPreviousSession = false;
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// Tell-Tabs section.
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_TellTabs);
|
|
|
|
var preload =
|
|
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
|
if (
|
|
ImGui.SliderInt(
|
|
HellionStrings.Wizard_Step3_AutoTellTabsHistoryPreload_Label,
|
|
ref preload,
|
|
0,
|
|
100
|
|
)
|
|
)
|
|
_state.PendingAutoTellTabsHistoryPreload = preload;
|
|
|
|
ImGui.Spacing();
|
|
|
|
// Visual section.
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_Visual);
|
|
|
|
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact))
|
|
_state.PendingUseCompactDensity = compact;
|
|
|
|
var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps;
|
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_PrettierTimestamps_Label, ref pretty))
|
|
_state.PendingPrettierTimestamps = pretty;
|
|
|
|
// Theme dropdown — built-ins only. Custom themes are power-user
|
|
// territory and would clutter the first-run flow.
|
|
var currentSlug = _state.PendingTheme ?? Plugin.Config.Theme;
|
|
var builtIns = Plugin.ThemeRegistry.AllBuiltIns().ToList();
|
|
var currentIndex = builtIns.FindIndex(t =>
|
|
string.Equals(t.Slug, currentSlug, StringComparison.OrdinalIgnoreCase)
|
|
);
|
|
if (currentIndex < 0)
|
|
currentIndex = 0;
|
|
|
|
using (
|
|
var combo = ImRaii.Combo(
|
|
HellionStrings.Wizard_Step3_Theme_Label,
|
|
builtIns[currentIndex].Name
|
|
)
|
|
)
|
|
{
|
|
if (combo.Success)
|
|
{
|
|
for (var i = 0; i < builtIns.Count; i++)
|
|
{
|
|
var isSelected = i == currentIndex;
|
|
if (ImGui.Selectable(builtIns[i].Name, isSelected))
|
|
_state.PendingTheme = builtIns[i].Slug;
|
|
if (isSelected)
|
|
ImGui.SetItemDefaultFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
DrawFooter(
|
|
showBack: true,
|
|
showSkip: true,
|
|
HellionStrings.Wizard_Nav_Next,
|
|
() => _state.CurrentStep = 4
|
|
);
|
|
}
|
|
|
|
private void DrawStepDone()
|
|
{
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step4_Title);
|
|
ImGui.Spacing();
|
|
|
|
// ✓ symbol, centred-ish via dummy padding.
|
|
var checkmark = "✓";
|
|
var checkSize = ImGui.CalcTextSize(checkmark);
|
|
var avail = ImGui.GetContentRegionAvail();
|
|
ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0));
|
|
ImGui.SameLine();
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
|
ImGui.TextUnformatted(checkmark);
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
// Summary card.
|
|
using (var summary = ImRaii.Child("##wizard-summary", new Vector2(-1, 130), true))
|
|
{
|
|
if (summary.Success)
|
|
{
|
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step4_SummaryHeading);
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
var profileLabel = _state.PendingProfile switch
|
|
{
|
|
PrivacyProfile.PrivacyFirst =>
|
|
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
|
PrivacyProfile.Casual => HellionStrings.Wizard_Profile_Casual_Heading,
|
|
PrivacyProfile.Roleplay => HellionStrings.Wizard_Profile_Roleplay_Heading,
|
|
PrivacyProfile.FullHistory => HellionStrings.Wizard_Profile_FullHistory_Heading,
|
|
_ => HellionStrings.Wizard_Step4_Summary_Unchanged,
|
|
};
|
|
ImGui.TextWrapped(
|
|
string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)
|
|
);
|
|
|
|
var historyLabel =
|
|
(_state.PendingLoadPreviousSession ?? false)
|
|
? HellionStrings.Wizard_Step3_LoadPreviousSession_Label
|
|
: HellionStrings.Wizard_Step4_Summary_Unchanged;
|
|
ImGui.TextWrapped(
|
|
string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)
|
|
);
|
|
|
|
var preloadValue =
|
|
_state.PendingAutoTellTabsHistoryPreload
|
|
?? Plugin.Config.AutoTellTabsHistoryPreload;
|
|
ImGui.TextWrapped(
|
|
string.Format(HellionStrings.Wizard_Step4_Summary_TellTabs, preloadValue)
|
|
);
|
|
|
|
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
|
var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps;
|
|
var themeSlug = _state.PendingTheme ?? Plugin.Config.Theme;
|
|
var themeName = Plugin.ThemeRegistry.Get(themeSlug).Name;
|
|
var visualParts = new List<string>();
|
|
if (compact)
|
|
visualParts.Add(HellionStrings.Wizard_Step3_UseCompactDensity_Label);
|
|
if (pretty)
|
|
visualParts.Add(HellionStrings.Wizard_Step3_PrettierTimestamps_Label);
|
|
visualParts.Add(themeName);
|
|
ImGui.TextWrapped(
|
|
string.Format(
|
|
HellionStrings.Wizard_Step4_Summary_Visual,
|
|
string.Join(", ", visualParts)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
|
|
// Inline FR-3 hint with placeholder for preload count.
|
|
var preloadForHint =
|
|
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
|
ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint));
|
|
|
|
ImGui.Spacing();
|
|
ImGui.TextDisabled(HellionStrings.Wizard_Step4_SettingsHint);
|
|
|
|
DrawFooter(
|
|
showBack: true,
|
|
showSkip: false,
|
|
HellionStrings.Wizard_Nav_Finish,
|
|
() =>
|
|
{
|
|
CommitPending();
|
|
Plugin.Config.FirstRunCompleted = true;
|
|
Plugin.SaveConfig();
|
|
IsOpen = false;
|
|
}
|
|
);
|
|
}
|
|
|
|
// Writes only non-null pending values back to Config. A null pending
|
|
// means the user did not touch that step's control, so the existing
|
|
// Config value is preserved. Theme switch goes through ThemeRegistry
|
|
// so the active palette updates live for the rest of the session.
|
|
internal void CommitPending()
|
|
{
|
|
switch (_state.PendingProfile)
|
|
{
|
|
case PrivacyProfile.PrivacyFirst:
|
|
ApplyPrivacyFirst();
|
|
break;
|
|
case PrivacyProfile.Casual:
|
|
ApplyCasual();
|
|
break;
|
|
case PrivacyProfile.Roleplay:
|
|
ApplyRoleplay();
|
|
break;
|
|
case PrivacyProfile.FullHistory:
|
|
ApplyFullHistory();
|
|
break;
|
|
}
|
|
|
|
if (_state.PendingLoadPreviousSession.HasValue)
|
|
Plugin.Config.LoadPreviousSession = _state.PendingLoadPreviousSession.Value;
|
|
|
|
if (_state.PendingFilterIncludePreviousSessions.HasValue)
|
|
Plugin.Config.FilterIncludePreviousSessions = _state
|
|
.PendingFilterIncludePreviousSessions
|
|
.Value;
|
|
|
|
if (_state.PendingAutoTellTabsHistoryPreload.HasValue)
|
|
Plugin.Config.AutoTellTabsHistoryPreload = _state
|
|
.PendingAutoTellTabsHistoryPreload
|
|
.Value;
|
|
|
|
if (_state.PendingUseCompactDensity.HasValue)
|
|
Plugin.Config.UseCompactDensity = _state.PendingUseCompactDensity.Value;
|
|
|
|
if (_state.PendingPrettierTimestamps.HasValue)
|
|
Plugin.Config.PrettierTimestamps = _state.PendingPrettierTimestamps.Value;
|
|
|
|
if (!string.IsNullOrWhiteSpace(_state.PendingTheme))
|
|
{
|
|
Plugin.Config.Theme = _state.PendingTheme;
|
|
Plugin.ThemeRegistry.Switch(_state.PendingTheme);
|
|
}
|
|
}
|
|
|
|
private void ApplyPrivacyFirst()
|
|
{
|
|
Plugin.Config.PrivacyFilterEnabled = true;
|
|
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.PrivacyFirstWhitelist];
|
|
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
|
|
|
Plugin.Config.RetentionEnabled = true;
|
|
Plugin.Config.RetentionDefaultDays = 30;
|
|
Plugin.Config.RetentionPerChannelDays = PrivacyDefaults.DefaultRetentionDays.ToDictionary(
|
|
p => p.Key,
|
|
p => p.Value
|
|
);
|
|
}
|
|
|
|
private void ApplyCasual()
|
|
{
|
|
Plugin.Config.PrivacyFilterEnabled = true;
|
|
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.CasualWhitelist];
|
|
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
|
|
|
Plugin.Config.RetentionEnabled = true;
|
|
Plugin.Config.RetentionDefaultDays = 30;
|
|
var policy = PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
|
|
foreach (var (type, days) in PrivacyDefaults.CasualRetentionOverrides)
|
|
policy[type] = days;
|
|
Plugin.Config.RetentionPerChannelDays = policy;
|
|
}
|
|
|
|
private void ApplyRoleplay()
|
|
{
|
|
Plugin.Config.PrivacyFilterEnabled = true;
|
|
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.RoleplayWhitelist];
|
|
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
|
|
|
Plugin.Config.RetentionEnabled = true;
|
|
Plugin.Config.RetentionDefaultDays = 30;
|
|
var policy = PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
|
|
foreach (var (type, days) in PrivacyDefaults.RoleplayRetentionOverrides)
|
|
policy[type] = days;
|
|
Plugin.Config.RetentionPerChannelDays = policy;
|
|
}
|
|
|
|
private void ApplyFullHistory()
|
|
{
|
|
// Full history = upstream Chat 2 behavior. Filter off, retention off,
|
|
// everything (except battle messages, which Chat 2 itself controls)
|
|
// accumulates indefinitely.
|
|
Plugin.Config.PrivacyFilterEnabled = false;
|
|
Plugin.Config.PrivacyPersistUnknownChannels = true;
|
|
|
|
Plugin.Config.RetentionEnabled = false;
|
|
Plugin.Config.RetentionPerChannelDays.Clear();
|
|
}
|
|
|
|
// Test-only entry point so SelfTests/WizardStateSmokeStep can advance
|
|
// the state machine without spawning ImGui input events.
|
|
internal void TestOnly_AdvanceTo(int step) =>
|
|
_state.CurrentStep = Math.Clamp(step, 1, TotalSteps);
|
|
|
|
// Test-only setter so the smoke-test can pin a profile selection
|
|
// without driving the ImGui card-click path.
|
|
internal void TestOnly_SetPendingProfile(PrivacyProfile profile) =>
|
|
_state.PendingProfile = profile;
|
|
|
|
internal enum PrivacyProfile
|
|
{
|
|
PrivacyFirst,
|
|
Casual,
|
|
Roleplay,
|
|
FullHistory,
|
|
}
|
|
|
|
private sealed class WizardState
|
|
{
|
|
public int CurrentStep { get; set; } = 1;
|
|
public PrivacyProfile? PendingProfile { get; set; }
|
|
public bool? PendingLoadPreviousSession { get; set; }
|
|
public bool? PendingFilterIncludePreviousSessions { get; set; }
|
|
public int? PendingAutoTellTabsHistoryPreload { get; set; }
|
|
public bool? PendingUseCompactDensity { get; set; }
|
|
public bool? PendingPrettierTimestamps { get; set; }
|
|
public string? PendingTheme { get; set; }
|
|
}
|
|
}
|