Merge branch 'feature/v1.5.2'
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
subtitle: "First-Run Wizard — neu in 4 Steps, Roleplay-Profil neu"
|
||||
versionsnatur: "UX-Patch"
|
||||
---
|
||||
- **Vier Steps statt Single-Page.** Der First-Run-Wizard öffnet jetzt in vier Bühnen: Willkommen → Privacy-Profil → Power-Settings → Fertig. Pagination-Dots in Forge-Bronze oben rechts, Back/Skip/Next im Footer. Standardgröße 720×480 (Min 600×400) und der Fuchs-Banner sitzt als zugeklappter TreeNode oben in Step 1, damit die Einleitung im Fokus bleibt.
|
||||
- **Neues Privacy-Profil „Roleplay".** Datensparsamkeit plus Sagen und beide Emote-Typen für Story-Logs. Schreien und Rufen bleiben außen vor, Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage. Privacy-Picker wird zum 2×2-Grid, Casual bleibt mit ★-Marker als Empfehlung.
|
||||
- **Power-Settings sichtbar.** Bislang versteckte Defaults bekommen eine eigene Bühne: Vorherige Session laden, Filter inkl. alter Messages, N Tell-Messages vorladen, Compact-Density, Prettier-Timestamps und Theme-Picker für die 10 Built-in-Themes. Keine neuen Settings, nur das Bestehende sauber sichtbar.
|
||||
- **Staged-Commit und Test-Hint auf der Fertig-Bühne.** Auswahl wird erst beim Klick auf „Fertig ✓" geschrieben. „Später entscheiden" oder X-Close lässt die bestehende Config unangetastet, ein nicht angefasster Step behält die alten Werte. Direkt darunter sichtbar: „Tipp /tell <Spielername>", plus die aktuelle Preload-Zahl aus Step 3 als Hinweis auf den Auto-Tell-Tab-Spawn.
|
||||
- **Bestehende User sehen den neuen Wizard einmal.** Wer schon v1.5.1 hatte, bekommt den Multi-Step-Flow beim ersten v1.5.2-Boot aufgepoppt. Neues Config-Feld `WizardLastShownVersion` triggert das einmalig pro Wizard-Rework; Skip oder Finish reicht und danach öffnet er nicht mehr automatisch.
|
||||
- **Unter der Haube.** Pure-Helper-Tests für alle vier Profile-Sets in der Build-Suite (zwölf neue Facts), plus ein WizardStateSmokeStep für `/xlperf`. Migration v17 bleibt, nur ein optionales Config-Feld kommt dazu.
|
||||
@@ -100,6 +100,15 @@ public class Configuration : IPluginConfiguration
|
||||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
||||
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
||||
public bool FirstRunCompleted;
|
||||
|
||||
// Tracks which plugin version last surfaced the first-run wizard.
|
||||
// When the running version is newer than this, Plugin.LoadAsync
|
||||
// re-opens the wizard once so existing users see major UX reworks
|
||||
// (e.g. the v1.5.2 multi-step rewrite). Skip path and Finish both
|
||||
// set FirstRunCompleted = true on close, so the wizard only fires
|
||||
// once per version bump even if the user dismisses it.
|
||||
public string WizardLastShownVersion = string.Empty;
|
||||
|
||||
public bool UseHellionFont = true;
|
||||
public bool ShowHonorificTitleInHeader = true;
|
||||
|
||||
@@ -336,6 +345,7 @@ public class Configuration : IPluginConfiguration
|
||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
WizardLastShownVersion = other.WizardLastShownVersion;
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.5.1</Version>
|
||||
<Version>1.5.2</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
|
||||
@@ -35,6 +35,56 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**v1.5.2 — First-Run Wizard Rework (2026-05-18)**
|
||||
|
||||
UX patch. The first-run wizard becomes a four-step flow with a
|
||||
new Roleplay privacy profile and a power-settings step that
|
||||
surfaces previously-hidden defaults. Existing v1.5.1 users see
|
||||
the new wizard once on first v1.5.2 boot.
|
||||
|
||||
What changes user-visible:
|
||||
|
||||
- Wizard navigation: Welcome → Privacy profile → Power settings
|
||||
→ Done. Forge-Bronze pagination dots, dedicated stage for the
|
||||
power settings so they are no longer buried in Settings.
|
||||
- Fourth privacy profile "Roleplay": Privacy-First plus Say and
|
||||
both emote types, with a 30-day window for Say and a 90-day
|
||||
window for emotes. Shout, Yell and Novice Network stay out.
|
||||
- Privacy picker becomes a 2x2 grid. Casual stays the
|
||||
recommended option with a ★ marker.
|
||||
- Power-settings step covers Load Previous Session, Filter
|
||||
Include Previous Sessions, Auto-Tell-Tabs History Preload,
|
||||
Compact Density, Prettier Timestamps and a built-in theme
|
||||
picker. All six map to existing Configuration fields — no new
|
||||
settings introduced.
|
||||
- Staged commit: the wizard only writes to Config on the Finish
|
||||
step. Decide-later or X-close at any point leaves the existing
|
||||
config untouched.
|
||||
- Inline test hint on the done step: "type /tell <Player Name>
|
||||
into chat" surfaces the auto-tell-tab spawn mechanism.
|
||||
- Window starts at 720x480 (was 900x560) and can shrink to
|
||||
600x400; Step 1 keeps the fox banner in a folded TreeNode so
|
||||
the onboarding copy stays primary.
|
||||
- Existing users get the new wizard surfaced once on first boot
|
||||
after the update via the new WizardLastShownVersion config
|
||||
field. Future cycles bump the constant only when the wizard
|
||||
itself changes shape.
|
||||
|
||||
Under the hood:
|
||||
|
||||
- WizardStateSmokeStep added to /xlperf alongside the FontManager
|
||||
and ThemeSwitch self-tests.
|
||||
- Twelve new pure-helper xUnit Facts in the Build Suite cover
|
||||
all four privacy profile sets and their retention overrides.
|
||||
|
||||
Migration v17 stays (no schema bump). The Configuration grows
|
||||
one optional string field (WizardLastShownVersion) which
|
||||
defaults to empty for legacy users.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
|
||||
|
||||
Hybrid FontManager refactor plus an embedded provenance mark.
|
||||
@@ -177,42 +227,4 @@ changelog: |-
|
||||
|
||||
---
|
||||
|
||||
**v1.4.9 — Plugin-Load Render Polish (2026-05-15)**
|
||||
|
||||
Tenth sub-patch of the v1.4.x polish-sweep series. First-frame
|
||||
render cost drops from ~127 ms median to ~76 ms median,
|
||||
comfortably under Dalamud's 100 ms HITCH warning threshold.
|
||||
|
||||
- First-frame defer: six non-essential rendering sections inside
|
||||
ChatLogWindow skip their first Draw and run one frame later
|
||||
(bottom status bar, channel-name SeString chunks, window bounds
|
||||
check, v0.6.1 hint banner, autocomplete, input-preview
|
||||
calculation). User-visible delay is ~17 ms at 60 fps, hidden
|
||||
inside the post-reload font-atlas build window.
|
||||
- Slash-command centralisation: /hellion, /hellionView,
|
||||
/hellionSeString and /hellionDebugger are registered in
|
||||
LoadAsync instead of inside the corresponding window
|
||||
constructors. The plugin-manager Open and configuration buttons
|
||||
hang on the same path.
|
||||
- Plugin-load profiling logs stay on at Information level
|
||||
(MessageStore connect/migrate, FilterAllTabs, auto-translate
|
||||
warmup) as a regression tripwire — a future load past 100 ms
|
||||
will show up in /xllog without a Debug filter.
|
||||
- ChatTwo IPC compatibility layer: HellionChat now mirrors
|
||||
ChatTwo's full IPC surface (GetChatInputState,
|
||||
ChatInputStateChanged, Register, Unregister, Available,
|
||||
Invoke) under the ChatTwo.* namespace in addition to our
|
||||
existing HellionChat.* provider gates. Third-party
|
||||
integrations that historically only subscribe to ChatTwo's
|
||||
IPC — for example Artisan's and AllaganTools' context-menu
|
||||
hooks — keep working without requiring a code change on their
|
||||
side. Conflict detection prevents ChatTwo from loading in
|
||||
parallel with HellionChat, so there is no slot-collision risk
|
||||
at runtime.
|
||||
- Migration v17 stays (no schema bump).
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
|
||||
@@ -321,8 +321,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||
new SelfTests.FontManagerCtorSmokeStep(this),
|
||||
new SelfTests.FontPushSmokeStep(this),
|
||||
new SelfTests.WizardStateSmokeStep(this),
|
||||
]);
|
||||
|
||||
// Re-surface the wizard for existing users when a major UX
|
||||
// rework ships. The constant tracks the most recent version
|
||||
// whose wizard should be shown once; bump it in future cycles
|
||||
// that reshape the onboarding flow. Saved immediately so a
|
||||
// pre-Finish crash doesn't loop the prompt forever.
|
||||
const string WizardReshowVersion = "1.5.2";
|
||||
if (Config.WizardLastShownVersion != WizardReshowVersion)
|
||||
{
|
||||
Config.FirstRunCompleted = false;
|
||||
Config.WizardLastShownVersion = WizardReshowVersion;
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
if (!Config.FirstRunCompleted)
|
||||
FirstRunWizard.IsOpen = true;
|
||||
|
||||
|
||||
@@ -114,4 +114,29 @@ internal static class PrivacyDefaults
|
||||
[ChatType.StandardEmote] = 1,
|
||||
[ChatType.NoviceNetwork] = 1,
|
||||
};
|
||||
|
||||
// Roleplay: Privacy-First + Say + both emote types. Public-distance
|
||||
// channels (Shout, Yell) stay out — they are public-noise from
|
||||
// strangers, not story content. Novice Network also stays out;
|
||||
// it is not RP-adjacent and would dilute the profile's intent.
|
||||
internal static readonly IReadOnlySet<ChatType> RoleplayWhitelist = new HashSet<ChatType>(
|
||||
PrivacyFirstWhitelist
|
||||
)
|
||||
{
|
||||
ChatType.Say,
|
||||
ChatType.CustomEmote,
|
||||
ChatType.StandardEmote,
|
||||
};
|
||||
|
||||
// RP sessions function as story logs: Say + emotes need a longer
|
||||
// window than Casual's 1-day public-chat window. 30 days for Say
|
||||
// keeps in-character dialogue scrollable across multiple sessions,
|
||||
// 90 days for emotes mirrors the Privacy-First conversation default.
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> RoleplayRetentionOverrides =
|
||||
new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.Say] = 30,
|
||||
[ChatType.CustomEmote] = 90,
|
||||
[ChatType.StandardEmote] = 90,
|
||||
};
|
||||
}
|
||||
|
||||
+32
@@ -116,6 +116,38 @@ internal class HellionStrings
|
||||
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
||||
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
||||
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
||||
internal static string Wizard_Step1_Title => Get(nameof(Wizard_Step1_Title));
|
||||
internal static string Wizard_Step1_Subtitle => Get(nameof(Wizard_Step1_Subtitle));
|
||||
internal static string Wizard_Step1_Footer_Hint => Get(nameof(Wizard_Step1_Footer_Hint));
|
||||
internal static string Wizard_Step1_Skip_Label => Get(nameof(Wizard_Step1_Skip_Label));
|
||||
internal static string Wizard_Step1_Skip_Tooltip => Get(nameof(Wizard_Step1_Skip_Tooltip));
|
||||
internal static string Wizard_Step2_Title => Get(nameof(Wizard_Step2_Title));
|
||||
internal static string Wizard_Step2_RecommendedFooter => Get(nameof(Wizard_Step2_RecommendedFooter));
|
||||
internal static string Wizard_Profile_Roleplay_Heading => Get(nameof(Wizard_Profile_Roleplay_Heading));
|
||||
internal static string Wizard_Profile_Roleplay_Description => Get(nameof(Wizard_Profile_Roleplay_Description));
|
||||
internal static string Wizard_Profile_Roleplay_Apply => Get(nameof(Wizard_Profile_Roleplay_Apply));
|
||||
internal static string Wizard_Nav_Back => Get(nameof(Wizard_Nav_Back));
|
||||
internal static string Wizard_Nav_Next => Get(nameof(Wizard_Nav_Next));
|
||||
internal static string Wizard_Nav_Finish => Get(nameof(Wizard_Nav_Finish));
|
||||
internal static string Wizard_Step3_Title => Get(nameof(Wizard_Step3_Title));
|
||||
internal static string Wizard_Step3_Section_History => Get(nameof(Wizard_Step3_Section_History));
|
||||
internal static string Wizard_Step3_Section_TellTabs => Get(nameof(Wizard_Step3_Section_TellTabs));
|
||||
internal static string Wizard_Step3_Section_Visual => Get(nameof(Wizard_Step3_Section_Visual));
|
||||
internal static string Wizard_Step3_LoadPreviousSession_Label => Get(nameof(Wizard_Step3_LoadPreviousSession_Label));
|
||||
internal static string Wizard_Step3_FilterIncludePreviousSessions_Label => Get(nameof(Wizard_Step3_FilterIncludePreviousSessions_Label));
|
||||
internal static string Wizard_Step3_AutoTellTabsHistoryPreload_Label => Get(nameof(Wizard_Step3_AutoTellTabsHistoryPreload_Label));
|
||||
internal static string Wizard_Step3_UseCompactDensity_Label => Get(nameof(Wizard_Step3_UseCompactDensity_Label));
|
||||
internal static string Wizard_Step3_PrettierTimestamps_Label => Get(nameof(Wizard_Step3_PrettierTimestamps_Label));
|
||||
internal static string Wizard_Step3_Theme_Label => Get(nameof(Wizard_Step3_Theme_Label));
|
||||
internal static string Wizard_Step4_Title => Get(nameof(Wizard_Step4_Title));
|
||||
internal static string Wizard_Step4_SummaryHeading => Get(nameof(Wizard_Step4_SummaryHeading));
|
||||
internal static string Wizard_Step4_Summary_Profile => Get(nameof(Wizard_Step4_Summary_Profile));
|
||||
internal static string Wizard_Step4_Summary_History => Get(nameof(Wizard_Step4_Summary_History));
|
||||
internal static string Wizard_Step4_Summary_TellTabs => Get(nameof(Wizard_Step4_Summary_TellTabs));
|
||||
internal static string Wizard_Step4_Summary_Visual => Get(nameof(Wizard_Step4_Summary_Visual));
|
||||
internal static string Wizard_Step4_Summary_Unchanged => Get(nameof(Wizard_Step4_Summary_Unchanged));
|
||||
internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint));
|
||||
internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint));
|
||||
|
||||
internal static string Export_Heading => Get(nameof(Export_Heading));
|
||||
internal static string Export_Help => Get(nameof(Export_Help));
|
||||
|
||||
@@ -228,6 +228,102 @@
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Title" xml:space="preserve">
|
||||
<value>Willkommen bei Hellion Chat</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Subtitle" xml:space="preserve">
|
||||
<value>Ein Chat 2 Fork von Hellion Forge mit DSGVO-konformen Defaults, brand-konsistentem Look und Quality-of-Life-Verbesserungen.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Footer_Hint" xml:space="preserve">
|
||||
<value>3 kurze Schritte. Du kannst alles später unter Einstellungen → Hellion Chat ändern.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Skip_Label" xml:space="preserve">
|
||||
<value>Später entscheiden</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Skip_Tooltip" xml:space="preserve">
|
||||
<value>Assistenten schließen. Die Plugin-Standardwerte bleiben aktiv. Du kannst den Assistenten über Einstellungen → Hellion Chat erneut öffnen.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step2_Title" xml:space="preserve">
|
||||
<value>Was darf gespeichert werden?</value>
|
||||
</data>
|
||||
<data name="Wizard_Step2_RecommendedFooter" xml:space="preserve">
|
||||
<value>★ = empfohlen für die meisten Spieler.</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Heading" xml:space="preserve">
|
||||
<value>Roleplay</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Description" xml:space="preserve">
|
||||
<value>Wie Datensparsamkeit, plus Sagen und beide Emote-Typen für deine Story-Logs. Schreien und Rufen bleiben außen vor — Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage.</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Apply" xml:space="preserve">
|
||||
<value>Roleplay übernehmen</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Back" xml:space="preserve">
|
||||
<value>‹ Zurück</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Next" xml:space="preserve">
|
||||
<value>Weiter ›</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Finish" xml:space="preserve">
|
||||
<value>Fertig ✓</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Title" xml:space="preserve">
|
||||
<value>Versteckte Defaults</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_History" xml:space="preserve">
|
||||
<value>Verlauf</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_TellTabs" xml:space="preserve">
|
||||
<value>Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_Visual" xml:space="preserve">
|
||||
<value>Optik</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_LoadPreviousSession_Label" xml:space="preserve">
|
||||
<value>Vorherige Session beim Start laden</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_FilterIncludePreviousSessions_Label" xml:space="preserve">
|
||||
<value>Filter auch auf alte Messages anwenden</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_AutoTellTabsHistoryPreload_Label" xml:space="preserve">
|
||||
<value>N Tell-Messages beim Öffnen eines Auto-Tabs vorladen</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_UseCompactDensity_Label" xml:space="preserve">
|
||||
<value>Kompakter Density-Modus</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_PrettierTimestamps_Label" xml:space="preserve">
|
||||
<value>Schönere Timestamps (relative Zeit)</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Theme_Label" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Title" xml:space="preserve">
|
||||
<value>Du bist startklar</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_SummaryHeading" xml:space="preserve">
|
||||
<value>Deine Konfiguration</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Profile" xml:space="preserve">
|
||||
<value>Profil: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_History" xml:space="preserve">
|
||||
<value>Verlauf: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_TellTabs" xml:space="preserve">
|
||||
<value>Tell-Tabs: {0} Messages vorladen</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Visual" xml:space="preserve">
|
||||
<value>Optik: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Unchanged" xml:space="preserve">
|
||||
<value>(unverändert)</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_TestHint" xml:space="preserve">
|
||||
<value>💡 Probier's aus: Tipp /tell <Spielername> in den Chat. Hellion Chat öffnet automatisch einen eigenen Tab für die Unterhaltung und lädt die letzten {0} Messages mit.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_SettingsHint" xml:space="preserve">
|
||||
<value>Einstellungen → Hellion Chat zum späteren Anpassen</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
||||
</data>
|
||||
|
||||
@@ -228,6 +228,102 @@
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Title" xml:space="preserve">
|
||||
<value>Welcome to Hellion Chat</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Subtitle" xml:space="preserve">
|
||||
<value>A Chat 2 fork from Hellion Forge with privacy-aware defaults, brand-consistent visuals, and a few quality-of-life touches.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Footer_Hint" xml:space="preserve">
|
||||
<value>Three short steps. You can change everything later under Settings → Hellion Chat.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Skip_Label" xml:space="preserve">
|
||||
<value>Decide later</value>
|
||||
</data>
|
||||
<data name="Wizard_Step1_Skip_Tooltip" xml:space="preserve">
|
||||
<value>Close the wizard. The plugin defaults stay active. You can reopen the wizard from Settings → Hellion Chat.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step2_Title" xml:space="preserve">
|
||||
<value>What gets stored?</value>
|
||||
</data>
|
||||
<data name="Wizard_Step2_RecommendedFooter" xml:space="preserve">
|
||||
<value>★ = recommended for most players.</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Heading" xml:space="preserve">
|
||||
<value>Roleplay</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Description" xml:space="preserve">
|
||||
<value>Like Privacy First, plus Say and both emote types for your story logs. Shout and Yell stay out — public-distance noise from strangers is not story content. Retention: Say 30 days, emotes 90 days.</value>
|
||||
</data>
|
||||
<data name="Wizard_Profile_Roleplay_Apply" xml:space="preserve">
|
||||
<value>Apply roleplay</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Back" xml:space="preserve">
|
||||
<value>‹ Back</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Next" xml:space="preserve">
|
||||
<value>Next ›</value>
|
||||
</data>
|
||||
<data name="Wizard_Nav_Finish" xml:space="preserve">
|
||||
<value>Finish ✓</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Title" xml:space="preserve">
|
||||
<value>Hidden defaults</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_History" xml:space="preserve">
|
||||
<value>History</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_TellTabs" xml:space="preserve">
|
||||
<value>Tell tabs</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Section_Visual" xml:space="preserve">
|
||||
<value>Visual</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_LoadPreviousSession_Label" xml:space="preserve">
|
||||
<value>Load previous session on startup</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_FilterIncludePreviousSessions_Label" xml:space="preserve">
|
||||
<value>Apply filters to messages from previous sessions</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_AutoTellTabsHistoryPreload_Label" xml:space="preserve">
|
||||
<value>Preload N tell messages when an auto-tab opens</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_UseCompactDensity_Label" xml:space="preserve">
|
||||
<value>Compact density</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_PrettierTimestamps_Label" xml:space="preserve">
|
||||
<value>Prettier timestamps (relative time)</value>
|
||||
</data>
|
||||
<data name="Wizard_Step3_Theme_Label" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Title" xml:space="preserve">
|
||||
<value>You're all set</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_SummaryHeading" xml:space="preserve">
|
||||
<value>Your configuration</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Profile" xml:space="preserve">
|
||||
<value>Profile: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_History" xml:space="preserve">
|
||||
<value>History: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_TellTabs" xml:space="preserve">
|
||||
<value>Tell tabs: preload {0} messages</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Visual" xml:space="preserve">
|
||||
<value>Visual: {0}</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_Summary_Unchanged" xml:space="preserve">
|
||||
<value>(unchanged)</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_TestHint" xml:space="preserve">
|
||||
<value>💡 Try it: type /tell <Player Name> into chat. Hellion Chat opens a dedicated tab for the conversation and preloads the last {0} messages.</value>
|
||||
</data>
|
||||
<data name="Wizard_Step4_SettingsHint" xml:space="preserve">
|
||||
<value>Settings → Hellion Chat to fine-tune later</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (GDPR Art. 15 — Right of access)</value>
|
||||
</data>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Ui;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Drives the FirstRunWizard state machine through every step and
|
||||
// commits a no-op pending state (Variant 1), then re-runs picking
|
||||
// Roleplay on Step 2 and skipping Step 3 (Variant 2). Verifies
|
||||
// that the staged-commit path does not throw under any combination
|
||||
// of Pending* values and that CommitPending leaves Config in a
|
||||
// readable shape. Variant 2's Roleplay commit would normally
|
||||
// mutate the six PrivacyFilter / Retention fields ApplyRoleplay
|
||||
// touches, so the step snapshots them before Variant 2 runs and
|
||||
// CleanUp() restores them — the self-test stays idempotent across
|
||||
// repeated /xlperf runs and does not overwrite an active privacy
|
||||
// profile.
|
||||
internal sealed class WizardStateSmokeStep : ISelfTestStep
|
||||
{
|
||||
private readonly Plugin plugin;
|
||||
|
||||
// Snapshot slots for the six Configuration fields ApplyRoleplay
|
||||
// writes in Variant 2. Populated right before Variant 2 mutates
|
||||
// Config, consumed by CleanUp(). Reference-typed snapshots
|
||||
// (HashSet, Dictionary) capture the existing slot by reference,
|
||||
// which is safe because ApplyRoleplay reassigns the slot with
|
||||
// a fresh instance instead of mutating in place.
|
||||
private bool? snapshotPrivacyFilterEnabled;
|
||||
private HashSet<ChatType>? snapshotPrivacyPersistChannels;
|
||||
private bool? snapshotPrivacyPersistUnknownChannels;
|
||||
private bool? snapshotRetentionEnabled;
|
||||
private int? snapshotRetentionDefaultDays;
|
||||
private Dictionary<ChatType, int>? snapshotRetentionPerChannelDays;
|
||||
|
||||
public WizardStateSmokeStep(Plugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public string Name => "Hellion Chat - FirstRunWizard state smoke";
|
||||
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
var wizard = this.plugin.FirstRunWizard;
|
||||
if (wizard is null)
|
||||
{
|
||||
ImGui.Text("Plugin.FirstRunWizard is null");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Variant 1: no-op CommitPending. Walks the state machine and
|
||||
// verifies the empty-pending write-back path does not throw.
|
||||
wizard.TestOnly_AdvanceTo(1);
|
||||
wizard.TestOnly_AdvanceTo(2);
|
||||
wizard.TestOnly_AdvanceTo(3);
|
||||
wizard.TestOnly_AdvanceTo(4);
|
||||
wizard.CommitPending();
|
||||
|
||||
// Variant 2: skip Step 3 explicitly. Picks Roleplay on Step 2,
|
||||
// jumps straight to Step 4 (no Step-3 entry → no seed for
|
||||
// LoadPreviousSession / FilterIncludePreviousSessions), commits,
|
||||
// and asserts the two coupled history toggles remained on their
|
||||
// pre-test value. Pins the null-semantics from Spec Z.176 so a
|
||||
// regression in CommitPending that started writing seeded
|
||||
// recommendations unconditionally would surface here.
|
||||
// CommitPending → ApplyRoleplay overwrites six privacy /
|
||||
// retention fields, so snapshot them first and let CleanUp
|
||||
// restore them after the assert. Keeps /xlperf idempotent.
|
||||
this.snapshotPrivacyFilterEnabled = Plugin.Config.PrivacyFilterEnabled;
|
||||
this.snapshotPrivacyPersistChannels = Plugin.Config.PrivacyPersistChannels;
|
||||
this.snapshotPrivacyPersistUnknownChannels = Plugin
|
||||
.Config
|
||||
.PrivacyPersistUnknownChannels;
|
||||
this.snapshotRetentionEnabled = Plugin.Config.RetentionEnabled;
|
||||
this.snapshotRetentionDefaultDays = Plugin.Config.RetentionDefaultDays;
|
||||
this.snapshotRetentionPerChannelDays = Plugin.Config.RetentionPerChannelDays;
|
||||
|
||||
var loadPrevBefore = Plugin.Config.LoadPreviousSession;
|
||||
var filterPrevBefore = Plugin.Config.FilterIncludePreviousSessions;
|
||||
wizard.TestOnly_AdvanceTo(2);
|
||||
wizard.TestOnly_SetPendingProfile(FirstRunWizard.PrivacyProfile.Roleplay);
|
||||
wizard.TestOnly_AdvanceTo(4);
|
||||
wizard.CommitPending();
|
||||
if (Plugin.Config.LoadPreviousSession != loadPrevBefore)
|
||||
{
|
||||
ImGui.Text("Skip-Step-3 path overwrote LoadPreviousSession");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
if (Plugin.Config.FilterIncludePreviousSessions != filterPrevBefore)
|
||||
{
|
||||
ImGui.Text("Skip-Step-3 path overwrote FilterIncludePreviousSessions");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ImGui.Text($"Wizard state smoke threw: {ex.GetType().Name}: {ex.Message}");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
public void CleanUp()
|
||||
{
|
||||
// Restore the six Variant-2 snapshots so back-to-back /xlperf
|
||||
// runs don't drift the active privacy profile. If Variant 2
|
||||
// never ran (Variant 1 threw early), the slots stay null and
|
||||
// restore is a no-op. After restore the slots are nulled so a
|
||||
// future RunStep starts fresh.
|
||||
if (this.snapshotPrivacyFilterEnabled is { } privacyFilter)
|
||||
Plugin.Config.PrivacyFilterEnabled = privacyFilter;
|
||||
if (this.snapshotPrivacyPersistChannels is { } persistChannels)
|
||||
Plugin.Config.PrivacyPersistChannels = persistChannels;
|
||||
if (this.snapshotPrivacyPersistUnknownChannels is { } persistUnknown)
|
||||
Plugin.Config.PrivacyPersistUnknownChannels = persistUnknown;
|
||||
if (this.snapshotRetentionEnabled is { } retentionEnabled)
|
||||
Plugin.Config.RetentionEnabled = retentionEnabled;
|
||||
if (this.snapshotRetentionDefaultDays is { } retentionDays)
|
||||
Plugin.Config.RetentionDefaultDays = retentionDays;
|
||||
if (this.snapshotRetentionPerChannelDays is { } retentionPolicy)
|
||||
Plugin.Config.RetentionPerChannelDays = retentionPolicy;
|
||||
|
||||
this.snapshotPrivacyFilterEnabled = null;
|
||||
this.snapshotPrivacyPersistChannels = null;
|
||||
this.snapshotPrivacyPersistUnknownChannels = null;
|
||||
this.snapshotRetentionEnabled = null;
|
||||
this.snapshotRetentionDefaultDays = null;
|
||||
this.snapshotRetentionPerChannelDays = null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -6,13 +7,34 @@ 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")
|
||||
@@ -21,10 +43,10 @@ public sealed class FirstRunWizard : Window
|
||||
|
||||
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
||||
SizeCondition = ImGuiCond.Appearing;
|
||||
Size = new Vector2(900, 560);
|
||||
Size = new Vector2(720, 480);
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(720, 480),
|
||||
MinimumSize = new Vector2(600, 400),
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||
};
|
||||
}
|
||||
@@ -32,138 +54,536 @@ 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,
|
||||
// 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();
|
||||
|
||||
// Banner is opt-in: the full silhouette dominates the wizard window
|
||||
// at the default size, so the TreeNode is folded by default and the
|
||||
// onboarding copy stays the primary focus. Mirrors the pre-rewrite
|
||||
// collapsible anchor from v1.5.1.
|
||||
using (var tree = ImRaii.TreeNode("Hellion Forge"))
|
||||
{
|
||||
if (tree.Success)
|
||||
{
|
||||
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
||||
{
|
||||
// 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,
|
||||
string? warning,
|
||||
string buttonLabel,
|
||||
Action onApply
|
||||
bool recommended,
|
||||
float width,
|
||||
float height
|
||||
)
|
||||
{
|
||||
using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true);
|
||||
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 +614,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 +639,34 @@ 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.finalfantasyxiv.com/)
|
||||
@@ -11,7 +11,7 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</p>
|
||||
|
||||
**Version 1.5.1** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
**Version 1.5.2** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||
|
||||
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine
|
||||
@@ -299,6 +299,22 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
||||
|
||||
## Project Status
|
||||
|
||||
**Version 1.5.2** — First-Run Wizard Rework. The single-page wizard becomes a four-step
|
||||
staged-commit flow (Welcome → Privacy → Power Settings → Done). The privacy picker becomes a 2×2
|
||||
grid with a fourth profile "Roleplay" that extends Privacy-First with `Say` and both emote types
|
||||
under a 30-/90-day retention window. A power-settings stage surfaces six previously-hidden
|
||||
`Configuration` defaults in one place without introducing any new settings. The wizard window
|
||||
shrinks to 720×480 default (was 900×560, MinimumSize 600×400) after smoke feedback and Step 1
|
||||
keeps the fox banner in a folded TreeNode so the onboarding copy stays primary. Existing v1.5.1
|
||||
users see the new flow once on first v1.5.2 boot via a new `WizardLastShownVersion` config marker.
|
||||
Under the hood: a `WizardStateSmokeStep` joins `/xlperf`, the Build Suite gains twelve pure-helper
|
||||
xUnit Facts pinning all four privacy profile sets and the new Roleplay retention overrides.
|
||||
Migration v17 stays — `Configuration` only grows one optional string field.
|
||||
|
||||
---
|
||||
|
||||
### Project status (pre-v1.5.2, kept for context)
|
||||
|
||||
**Version 1.5.1** — FontAtlas Refactor and Hellion Forge Signature. The FontManager moves from the
|
||||
inherited Chat 2 anti-pattern (null! fields + a separate BuildFonts method) to a hybrid model where
|
||||
the game fonts and FontAwesome are init-only handles and only the user-configurable delegate fonts
|
||||
@@ -318,10 +334,6 @@ defer their font-atlas build to land at ~7 ms; Chat 2 + HellionChat were ~75 ms)
|
||||
cost lives in the UiBuilder first-frame render path, not in the atlas build. A first-frame render
|
||||
investigation is reserved for a later cycle.
|
||||
|
||||
---
|
||||
|
||||
### Project status (pre-v1.5.1, kept for context)
|
||||
|
||||
**Version 1.5.0** — DI Foundation and Service Refactor. Major architecture cycle: the plugin
|
||||
bootstrap moves to a generic-host DI container (`Microsoft.Extensions.Hosting` +
|
||||
`IServiceCollection`) modelled on Lightless Sync. All 18 instance-class services migrate from a
|
||||
|
||||
@@ -11,6 +11,53 @@ releases as an overview and links to the release pages for details.
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.5.2 — First-Run Wizard Rework (2026-05-18)
|
||||
|
||||
UX patch. The single-page first-run wizard becomes a four-step staged-commit flow, the privacy
|
||||
profile catalogue gains a fourth entry "Roleplay", and a new power-settings stage surfaces six
|
||||
previously-hidden Configuration defaults. Existing v1.5.1 users see the new wizard once on first
|
||||
v1.5.2 boot via a new `WizardLastShownVersion` config marker.
|
||||
|
||||
User-visible:
|
||||
|
||||
- Wizard layout: Welcome → Privacy profile → Power settings → Done. Forge-Bronze pagination dots,
|
||||
per-step Back / Decide later / Next footer. Decide-later and X-close both leave the existing
|
||||
config untouched; only the Finish ✓ click commits pending choices.
|
||||
- Fourth privacy profile "Roleplay": Privacy-First whitelist plus `Say` and both emote types, with a
|
||||
30-day retention window for `Say` and 90 days for the two emote channels. `Shout`, `Yell` and
|
||||
`NoviceNetwork` stay out — public-distance noise from strangers is not story content.
|
||||
- Privacy picker becomes a 2×2 grid. Casual stays the recommended option with a ★ marker.
|
||||
- Power-settings stage surfaces six existing `Configuration` fields in one place: Load Previous
|
||||
Session, Filter Include Previous Sessions, Auto-Tell-Tabs History Preload, Compact Density,
|
||||
Prettier Timestamps, plus a built-in theme picker. No new settings are introduced — the stage just
|
||||
collects what was previously buried in Settings → Privacy / Chat / Data Management / Appearance.
|
||||
- Inline test hint on the done stage: `type /tell <Player Name> into chat` surfaces the auto-tell-tab
|
||||
spawn mechanism for new users.
|
||||
- Wizard window starts at 720×480 (was 900×560) and can shrink to 600×400. Step 1 wraps the fox
|
||||
banner in a collapsible TreeNode, folded by default — onboarding copy stays primary.
|
||||
- Existing v1.5.1 users get the new wizard surfaced once on first v1.5.2 boot. A new
|
||||
`WizardLastShownVersion` config field tracks the most recent version whose wizard was shown;
|
||||
Plugin.LoadAsync resets `FirstRunCompleted` once when the constant `1.5.2` doesn't match.
|
||||
|
||||
Under the hood:
|
||||
|
||||
- `WizardStateSmokeStep` registered with `/xlperf`. Variant 1 walks the four steps with empty
|
||||
pending state to pin the no-op CommitPending path. Variant 2 picks Roleplay on Step 2, skips
|
||||
Step 3, commits, and asserts `LoadPreviousSession` / `FilterIncludePreviousSessions` stayed on
|
||||
their pre-test value — pinning the null-semantics contract. The step snapshots six privacy /
|
||||
retention fields before Variant 2 and `CleanUp()` restores them, so back-to-back runs don't drift
|
||||
the active profile.
|
||||
- Twelve pure-helper xUnit Facts in the Build Suite (`Privacy/PrivacyDefaultsTests.cs`) cover all
|
||||
four profile whitelists plus the new Roleplay retention overrides.
|
||||
- `Configuration` grows one optional string field `WizardLastShownVersion` (default empty). No
|
||||
schema bump — migration v17 still applies.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
[Full release notes on the Gitea release page.](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.5.2)
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)
|
||||
|
||||
Hybrid FontManager refactor plus an embedded Hellion Forge provenance mark.
|
||||
|
||||
+26
-7
@@ -10,14 +10,33 @@ be a poor fit for the plugin's privacy-first scope during brainstorming.
|
||||
|
||||
---
|
||||
|
||||
## Next Cycle (v1.5.2)
|
||||
## Next Cycle (v1.5.3)
|
||||
|
||||
**First-Run-Wizard rework with curated defaults beyond the three privacy profiles.** Jin's discovery
|
||||
in v1.4.10 surfaced the wizard's three-card layout as too thin — power users want richer presets out
|
||||
of the box. After that, FR localisation (Hezcal native-speaker review confirmed), then the Plugin
|
||||
Integrations Wave 2-6 (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM). The
|
||||
UiBuilder first-frame HITCH investigation that v1.5.1 surfaced sits as a separate spike near the
|
||||
Wine/Linux scroll-rubber-band investigation at the tail.
|
||||
**French localisation.** Strings from `Resources/HellionStrings.resx` get a FR translation pass
|
||||
(DeepL first draft), then Hezcal native-speaker review before release. After that, the Plugin
|
||||
Integrations Wave 2-6 (Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM) and the
|
||||
UiBuilder first-frame HITCH investigation that v1.5.1 surfaced are queued behind it, alongside the
|
||||
Wine/Linux scroll-rubber-band spike at the tail.
|
||||
|
||||
---
|
||||
|
||||
## v1.5.2 — First-Run Wizard Rework (released 2026-05-18)
|
||||
|
||||
Multi-step wizard replacement: Welcome → Privacy → Power Settings → Done with staged-commit so
|
||||
Decide-later or X-close at any point leaves the existing config untouched. New fourth privacy
|
||||
profile "Roleplay" extends Privacy-First with `Say` and both emote types under a 30-/90-day
|
||||
retention window. Privacy picker becomes a 2×2 grid; Casual keeps the ★ recommended marker. A new
|
||||
power-settings stage surfaces six previously-hidden `Configuration` fields (Load Previous Session,
|
||||
Filter Include Previous Sessions, Auto-Tell-Tabs History Preload, Compact Density, Prettier
|
||||
Timestamps, built-in theme picker) without introducing any new fields.
|
||||
|
||||
Window default size shrinks from 900×560 to 720×480 (MinimumSize 600×400) and Step 1 wraps the fox
|
||||
banner in a folded TreeNode after smoke feedback. Existing v1.5.1 users see the new wizard once on
|
||||
first v1.5.2 boot via a new `WizardLastShownVersion` config marker.
|
||||
|
||||
Under the hood: `WizardStateSmokeStep` joins the `/xlperf` lineup, the Build Suite gains twelve
|
||||
pure-helper xUnit Facts pinning all four privacy profile sets and the new Roleplay retention
|
||||
overrides. Migration v17 stays — `Configuration` only grows one optional string field.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user