diff --git a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs index 301661e8d..455b3fa2a 100644 --- a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs +++ b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using UnityEngine.UIElements; namespace Immutable.Audience.Samples.SampleApp @@ -38,6 +39,18 @@ internal readonly struct EventSpec private const string OptionalEnumSentinel = "(not set)"; + private static readonly string[] ProgressionStatusValues = + Enum.GetValues(typeof(ProgressionStatus)) + .Cast() + .Select(s => s.ToLowercaseString()) + .ToArray(); + + private static readonly string[] ResourceFlowValues = + Enum.GetValues(typeof(ResourceFlow)) + .Cast() + .Select(f => f.ToLowercaseString()) + .ToArray(); + // ---- Event catalogue ---- internal static readonly EventSpec[] Catalogue = @@ -63,7 +76,7 @@ internal readonly struct EventSpec // it as auto-tracked on Init with no public typed class; firing it // from the Send button would double-emit. new EventSpec(EventNames.Progression, new[] { - EventField.Enum(EventPropertyKeys.Status, new[] { "start", "complete", "fail" }), + EventField.Enum(EventPropertyKeys.Status, ProgressionStatusValues), EventField.Text(EventPropertyKeys.World, optional: true), EventField.Text(EventPropertyKeys.Level, optional: true), EventField.Text(EventPropertyKeys.Stage, optional: true), @@ -71,7 +84,7 @@ internal readonly struct EventSpec EventField.Number(EventPropertyKeys.DurationSec, optional: true), }), new EventSpec(EventNames.Resource, new[] { - EventField.Enum(EventPropertyKeys.Flow, new[] { "sink", "source" }), + EventField.Enum(EventPropertyKeys.Flow, ResourceFlowValues), EventField.Text(EventPropertyKeys.Currency), EventField.Number(EventPropertyKeys.Amount), EventField.Text(EventPropertyKeys.ItemType, optional: true), @@ -103,33 +116,33 @@ internal readonly struct EventSpec return new Progression { Status = ParseProgressionStatus(props), - World = OptionalString(props, "world"), - Level = OptionalString(props, "level"), - Stage = OptionalString(props, "stage"), - Score = OptionalInt(props, "score"), - DurationSec = OptionalFloat(props, "durationSec"), + World = OptionalString(props, EventPropertyKeys.World), + Level = OptionalString(props, EventPropertyKeys.Level), + Stage = OptionalString(props, EventPropertyKeys.Stage), + Score = OptionalInt(props, EventPropertyKeys.Score), + DurationSec = OptionalFloat(props, EventPropertyKeys.DurationSec), }; case EventNames.Resource: return new Resource { Flow = ParseResourceFlow(props), - Currency = OptionalString(props, "currency") ?? "", - Amount = OptionalFloat(props, "amount") ?? 0f, - ItemType = OptionalString(props, "itemType"), - ItemId = OptionalString(props, "itemId"), + Currency = OptionalString(props, EventPropertyKeys.Currency) ?? "", + Amount = OptionalFloat(props, EventPropertyKeys.Amount) ?? 0f, + ItemType = OptionalString(props, EventPropertyKeys.ItemType), + ItemId = OptionalString(props, EventPropertyKeys.ItemId), }; case EventNames.Purchase: return new Purchase { - Currency = OptionalString(props, "currency") ?? "", - Value = OptionalDecimal(props, "value") ?? 0m, - ItemId = OptionalString(props, "itemId"), - ItemName = OptionalString(props, "itemName"), - Quantity = OptionalInt(props, "quantity"), - TransactionId = OptionalString(props, "transactionId"), + Currency = OptionalString(props, EventPropertyKeys.Currency) ?? "", + Value = OptionalDecimal(props, EventPropertyKeys.Value) ?? 0m, + ItemId = OptionalString(props, EventPropertyKeys.ItemId), + ItemName = OptionalString(props, EventPropertyKeys.ItemName), + Quantity = OptionalInt(props, EventPropertyKeys.Quantity), + TransactionId = OptionalString(props, EventPropertyKeys.TransactionId), }; case EventNames.MilestoneReached: - return new MilestoneReached { Name = OptionalString(props, "name") ?? "" }; + return new MilestoneReached { Name = OptionalString(props, EventPropertyKeys.Name) ?? "" }; default: return null; } @@ -137,27 +150,20 @@ internal readonly struct EventSpec private static ProgressionStatus? ParseProgressionStatus(Dictionary props) { - var s = OptionalString(props, "status"); + var s = OptionalString(props, EventPropertyKeys.Status); if (string.IsNullOrEmpty(s)) return null; - return s switch - { - "start" => ProgressionStatus.Start, - "complete" => ProgressionStatus.Complete, - "fail" => ProgressionStatus.Fail, - _ => (ProgressionStatus?)null, - }; + foreach (ProgressionStatus value in Enum.GetValues(typeof(ProgressionStatus))) + if (value.ToLowercaseString() == s) return value; + return null; } private static ResourceFlow? ParseResourceFlow(Dictionary props) { - var s = OptionalString(props, "flow"); + var s = OptionalString(props, EventPropertyKeys.Flow); if (string.IsNullOrEmpty(s)) return null; - return s switch - { - "source" => ResourceFlow.Source, - "sink" => ResourceFlow.Sink, - _ => (ResourceFlow?)null, - }; + foreach (ResourceFlow value in Enum.GetValues(typeof(ResourceFlow))) + if (value.ToLowercaseString() == s) return value; + return null; } private static string? OptionalString(Dictionary props, string key) => diff --git a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs index 1c70fbd78..5b0a81c48 100644 --- a/examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs +++ b/examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs @@ -16,22 +16,42 @@ public sealed partial class AudienceSample // ---- Constants & tables ---- private static readonly ConsentLevel[] ConsentOrder = { ConsentLevel.None, ConsentLevel.Anonymous, ConsentLevel.Full }; - private static readonly string[] ConsentStateClass = { "state-err", "state-warn", "state-ok" }; + private static readonly string[] ConsentStateClass = { SampleAppUi.Css.StateErr, SampleAppUi.Css.StateWarn, SampleAppUi.Css.StateOk }; private static readonly (string TabId, string PanelId)[] Tabs = { - ("tab-setup", "panel-setup"), - ("tab-consent", "panel-consent"), - ("tab-typed-events", "panel-typed-events"), - ("tab-identity", "panel-identity"), + (SampleAppUi.Tabs.Setup, SampleAppUi.Panels.Setup), + (SampleAppUi.Tabs.Consent, SampleAppUi.Panels.Consent), + (SampleAppUi.Tabs.TypedEvents, SampleAppUi.Panels.TypedEvents), + (SampleAppUi.Tabs.Identity, SampleAppUi.Panels.Identity), }; - private static readonly string[] StateClasses = { "state-ok", "state-warn", "state-err", "dim" }; + private static readonly string[] StateClasses = { SampleAppUi.Css.StateOk, SampleAppUi.Css.StateWarn, SampleAppUi.Css.StateErr, SampleAppUi.Css.Dim }; private const int CollapseThreshold = 240; private const int StatusPollIntervalMs = 500; private const float NarrowBreakpointPx = 1024f; + // Log pane drag-resize bounds and main-column padding tracker. + private const float LogResizeMinHeight = 120f; + private const float LogResizeMaxHeight = 1400f; + private const float MainPaddingBottomPx = 64f; + // Pixel slack used when deciding whether the log was at the bottom + // before a content mutation; treated as "at bottom" if within this. + private const float LogScrollBottomEpsilonPx = 4f; + + // Toast / flash timings. + private const int CopyButtonRevertMs = 800; + private const int CopiedFlashDurationMs = 1500; + + // Local-time format for the per-row timestamp (header) and the + // round-trip ISO format used in the copy-to-clipboard payload. + private const string LogRowTimestampFormat = "HH:mm:ss.fff"; + + // ✓ glyph injected into the runtime Toggle so the checked state + // shows a tick rather than a bare coloured square. + private const string DebugToggleTickGlyph = "✓"; + // ---- UI document state ---- // All fields below are populated by BindElements before any other access. #pragma warning disable 8618 @@ -105,7 +125,7 @@ private void InitializeUi() _sdkVersionLabel.text = $"v{Immutable.Audience.Constants.LibraryVersion}"; OnSdkStateChanged(); - AppendLog("READY", "Sample app loaded. Paste a publishable key and click Init.", LogLevel.Info, LogSource.App); + AppendLog(SampleAppUi.LogLabels.Ready, SampleAppUi.Messages.Ready, LogLevel.Info, LogSource.App); // Status bar mirrors live SDK state (UserId, SessionId, QueueSize) — // poll instead of subscribing because the SDK doesn't expose changes. @@ -120,9 +140,9 @@ private bool LoadUiDocument() { var doc = GetComponent() ?? gameObject.AddComponent(); if (doc.panelSettings == null) - doc.panelSettings = Resources.Load("AudienceSampleAppPanelSettings"); + doc.panelSettings = Resources.Load(SampleAppUi.Resources.PanelSettings); - var tree = Resources.Load("AudienceSample"); + var tree = Resources.Load(SampleAppUi.Resources.SampleAppTree); if (tree == null) { Debug.LogError("[Audience Sample] missing Resources/AudienceSample.uxml"); @@ -137,7 +157,7 @@ private bool LoadUiDocument() _root.Clear(); tree.CloneTree(_root); - var uss = Resources.Load("AudienceSample"); + var uss = Resources.Load(SampleAppUi.Resources.SampleAppStyleSheet); if (uss != null) _root.styleSheets.Add(uss); return true; } @@ -150,71 +170,71 @@ private T Require(string name) where T : VisualElement => private void BindElements() { - _prodWarning = Require