feat(ui): rewrite FirstRunWizard as four-step staged-commit flow
Multi-step navigation (Welcome → Privacy → Power Settings → Done) with a nested WizardState holding nullable Pending* fields. Profile picker becomes a 2x2 grid covering all four privacy profiles (PrivacyFirst, Casual ★ recommended, Roleplay new, FullHistory). Power-settings step surfaces six previously-hidden Configuration fields (LoadPreviousSession, FilterIncludePreviousSessions, AutoTellTabsHistoryPreload, UseCompactDensity, PrettierTimestamps, Theme) without introducing new ones. ApplyRoleplay mirrors the existing Apply* methods, CommitPending writes only the non-null fields back so skipping a step preserves existing config. OnClose docstring updated to reflect the actual code path (both Decide-Later and Finish set FirstRunCompleted = true, the wizard does not reopen).
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -6,13 +7,29 @@ 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")
|
||||
@@ -32,138 +49,407 @@ public sealed class FirstRunWizard : Window
|
||||
public override void OnClose()
|
||||
{
|
||||
// OnClose fires on explicit X-click and on plugin dispose. We never
|
||||
// implicitly accept the defaults here — the explicit "Later" button
|
||||
// does that. If the user hasn't picked a profile yet, the wizard
|
||||
// reopens on the next plugin load.
|
||||
// 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()
|
||||
{
|
||||
DrawHellionForgeAnchor();
|
||||
DrawPagination();
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextWrapped(HellionStrings.Wizard_Intro);
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
var avail = ImGui.GetContentRegionAvail();
|
||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
||||
// Reserve room for the footer separator + cancel button below the cards.
|
||||
var footerReserve =
|
||||
ImGui.GetStyle().ItemSpacing.Y * 3
|
||||
+ ImGui.GetTextLineHeight()
|
||||
+ ImGui.GetFrameHeightWithSpacing();
|
||||
var cardHeight = avail.Y - footerReserve;
|
||||
|
||||
DrawCard(
|
||||
"privacy-first",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
|
||||
null,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Apply,
|
||||
ApplyPrivacyFirst
|
||||
);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCard(
|
||||
"casual",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_Casual_Heading,
|
||||
HellionStrings.Wizard_Profile_Casual_Description,
|
||||
null,
|
||||
HellionStrings.Wizard_Profile_Casual_Apply,
|
||||
ApplyCasual
|
||||
);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCard(
|
||||
"full-history",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Heading,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Description,
|
||||
HellionStrings.Wizard_Profile_FullHistory_GdprWarning,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
||||
ApplyFullHistory
|
||||
);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.Button(HellionStrings.Wizard_Cancel_Label))
|
||||
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_Cancel_Tooltip);
|
||||
ImGuiUtil.Tooltip(HellionStrings.Wizard_Step1_Skip_Tooltip);
|
||||
}
|
||||
|
||||
private void DrawCard(
|
||||
string id,
|
||||
float width,
|
||||
float height,
|
||||
string heading,
|
||||
string description,
|
||||
string? warning,
|
||||
string buttonLabel,
|
||||
Action onApply
|
||||
)
|
||||
// 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))
|
||||
{
|
||||
using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true);
|
||||
if (ImGui.Button($"{primaryLabel}##wizard-primary"))
|
||||
onPrimary();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawStepWelcome()
|
||||
{
|
||||
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
|
||||
ImGui.Spacing();
|
||||
|
||||
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
||||
{
|
||||
// Centre the banner across the available region. Spec Z.96
|
||||
// calls for a zentrierten Mono-Font-Block. CalcTextSize must
|
||||
// run inside the MonoFont push so the measurement matches the
|
||||
// glyph width actually used for rendering.
|
||||
var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner);
|
||||
ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f);
|
||||
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
ImGui.TextUnformatted(heading);
|
||||
// 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 DataManagement 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/DataManagement.cs:182-200.
|
||||
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();
|
||||
|
||||
ImGui.TextWrapped(description);
|
||||
|
||||
if (warning is not null)
|
||||
// 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();
|
||||
ImGuiUtil.WarningText(warning);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
// Push the button to the bottom of the card.
|
||||
var lineHeight = ImGui.GetFrameHeightWithSpacing();
|
||||
var remaining = ImGui.GetContentRegionAvail().Y - lineHeight;
|
||||
if (remaining > 0)
|
||||
ImGui.Dummy(new Vector2(0, remaining));
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.Button($"{buttonLabel}##{id}", new Vector2(-1, 0)))
|
||||
// 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, () =>
|
||||
{
|
||||
onApply();
|
||||
CommitPending();
|
||||
Plugin.Config.FirstRunCompleted = true;
|
||||
Plugin.SaveConfig();
|
||||
IsOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collapsible because the full silhouette is taller than the wizard
|
||||
// window — folded by default so the privacy cards stay the primary
|
||||
// focus, expandable for whoever wants the "about the makers" anchor.
|
||||
private void DrawHellionForgeAnchor()
|
||||
// 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()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Hellion Forge");
|
||||
if (!tree.Success)
|
||||
return;
|
||||
switch (_state.PendingProfile)
|
||||
{
|
||||
case PrivacyProfile.PrivacyFirst: ApplyPrivacyFirst(); break;
|
||||
case PrivacyProfile.Casual: ApplyCasual(); break;
|
||||
case PrivacyProfile.Roleplay: ApplyRoleplay(); break;
|
||||
case PrivacyProfile.FullHistory: ApplyFullHistory(); break;
|
||||
}
|
||||
|
||||
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
||||
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
||||
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()
|
||||
@@ -194,6 +480,20 @@ public sealed class FirstRunWizard : Window
|
||||
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,
|
||||
@@ -205,4 +505,32 @@ public sealed class FirstRunWizard : Window
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user