7 Commits
v0.2.0 ... main

162 changed files with 2414 additions and 409 deletions

View File

@@ -197,8 +197,17 @@ public static class Brisk
}
internal static void NotifyAuthExpired(BriskAuthExpiredException exception)
{
NotifyAuthExpired(exception, true);
}
internal static void NotifyAuthExpired(BriskAuthExpiredException exception, bool showDefaultPresenter)
{
if (showDefaultPresenter)
{
s_context?.ErrorPresenter?.ShowAuthExpired(exception);
}
OnAuthExpired?.Invoke(exception);
}
@@ -241,6 +250,15 @@ public static class Brisk
return;
}
if (storedSession.ExpiresAt.HasValue && storedSession.ExpiresAt.Value <= DateTimeOffset.UtcNow)
{
await HandleExpiredStartupSessionAsync(
context,
storedSession,
new BriskAuthExpiredException("Stored session expired before initialization."));
return;
}
context.Session.Update(
storedSession.AccessToken,
storedSession.ExpiresAt,
@@ -268,9 +286,7 @@ public static class Brisk
}
catch (BriskAuthExpiredException exception)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
NotifyAuthExpired(exception);
await HandleExpiredStartupSessionAsync(context, storedSession, exception);
}
catch (BriskBlockingException exception)
{
@@ -278,4 +294,36 @@ public static class Brisk
throw;
}
}
private static async Task HandleExpiredStartupSessionAsync(BriskContext context, BriskStoredSession storedSession, BriskAuthExpiredException exception)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
NotifyAuthExpired(exception, false);
if (!context.Options.AutoReloginOnInitialize || !HasStoredLoginIdentity(storedSession))
{
return;
}
try
{
await Auth.LoginWithUserIdAsync(storedSession.LoginProvider, storedSession.LoginUserId);
}
catch (BriskBlockingException)
{
throw;
}
catch
{
// 初始化期静默重登失败时,交给业务层后续第三方登录流程重新调用 Brisk.Auth。
}
}
private static bool HasStoredLoginIdentity(BriskStoredSession storedSession)
{
return storedSession != null
&& !string.IsNullOrWhiteSpace(storedSession.LoginProvider)
&& !string.IsNullOrWhiteSpace(storedSession.LoginUserId);
}
}

View File

@@ -1,26 +1,74 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;
[DefaultExecutionOrder(10000)]
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
{
private const string HostName = "BriskDefaultErrorDialog";
private const float OpenDuration = 0.24f;
private const float CloseDuration = 0.2f;
private static readonly Color Ink = ColorFromHex(0x05060a);
private static readonly Color InkSoft = ColorFromHex(0x1b1d24);
private static readonly Color Muted = ColorFromHex(0x5f6572);
private static readonly Color Line = ColorFromHex(0xdde2ea);
private static readonly Color Paper = ColorFromHex(0xffffff);
private static readonly Color Soft = ColorFromHex(0xf7f8fb);
private static readonly Color Blue = ColorFromHex(0x2563ff);
private static readonly Color Teal = ColorFromHex(0x23d6c8);
private static readonly Color Violet = ColorFromHex(0x8b5cff);
private static readonly Color Dark = ColorFromHex(0x050506);
private static BriskDefaultErrorDialog s_instance;
private string _title;
private string _message;
private string _confirmText;
private UIDocument _document;
private PanelSettings _panelSettings;
private VisualElement _root;
private VisualElement _stage;
private VisualElement _panel;
private Label _eyebrowLabel;
private Label _titleLabel;
private Label _messageLabel;
private VisualElement _spacer;
private VisualElement _divider;
private VisualElement _confirmButton;
private Label _confirmLabel;
private Action _onConfirm;
private Coroutine _animation;
private bool _visible;
private bool _closeOnBackdrop;
private bool _closeOnEscape;
private GUIStyle _legacyTitleStyle;
private GUIStyle _legacyMessageStyle;
private GUIStyle _legacyEyebrowStyle;
private GUIStyle _legacyButtonStyle;
private Texture2D _legacyButtonTexture;
private Texture2D _legacyButtonHoverTexture;
public static void Show(string title, string message, string confirmText, Action onConfirm = null)
internal static bool IsVisible => s_instance != null && s_instance._visible;
internal static void RenderTopmostLegacyOverlay()
{
if (s_instance == null || !s_instance._visible)
{
return;
}
s_instance.DrawLegacyOverlay();
}
public static void Show(string title, string message, string confirmText, Action onConfirm = null, bool closeOnBackdrop = false, bool closeOnEscape = false)
{
var host = EnsureInstance();
host._title = string.IsNullOrWhiteSpace(title) ? "Brisk" : title;
host._message = string.IsNullOrWhiteSpace(message) ? "Unknown error." : message;
host._confirmText = string.IsNullOrWhiteSpace(confirmText) ? "OK" : confirmText;
host._onConfirm = onConfirm;
host._visible = true;
host.ShowInternal(
string.IsNullOrWhiteSpace(title) ? "Brisk" : title,
string.IsNullOrWhiteSpace(message) ? "服务暂时不可用,请稍后再试。" : message,
string.IsNullOrWhiteSpace(confirmText) ? "知道了" : confirmText,
onConfirm,
closeOnBackdrop,
closeOnEscape);
}
private static BriskDefaultErrorDialog EnsureInstance()
@@ -46,6 +94,249 @@ internal sealed class BriskDefaultErrorDialog : MonoBehaviour
return s_instance;
}
private void ShowInternal(string title, string message, string confirmText, Action onConfirm, bool closeOnBackdrop, bool closeOnEscape)
{
EnsureDocument();
ApplySafeAreaPadding();
_titleLabel.text = title;
_messageLabel.text = message;
_confirmLabel.text = confirmText;
_onConfirm = onConfirm;
_closeOnBackdrop = closeOnBackdrop;
_closeOnEscape = closeOnEscape;
_visible = true;
StartDialogAnimation(PlayOpenAnimation());
}
private void EnsureDocument()
{
if (_document != null)
{
ApplyResponsiveLayout();
return;
}
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
_panelSettings.name = HostName + "PanelSettings";
_panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
_panelSettings.scale = 1f;
_panelSettings.sortingOrder = 32767f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = _panelSettings;
_document.sortingOrder = 32767f;
BuildVisualTree(_document.rootVisualElement);
ApplyResponsiveLayout();
}
private void BuildVisualTree(VisualElement documentRoot)
{
documentRoot.Clear();
documentRoot.pickingMode = PickingMode.Ignore;
documentRoot.style.position = Position.Absolute;
documentRoot.style.left = 0f;
documentRoot.style.top = 0f;
documentRoot.style.right = 0f;
documentRoot.style.bottom = 0f;
documentRoot.style.width = Length.Percent(100f);
documentRoot.style.height = Length.Percent(100f);
documentRoot.style.flexGrow = 1f;
_root = new VisualElement
{
pickingMode = PickingMode.Position,
focusable = true
};
_root.style.position = Position.Absolute;
_root.style.left = 0;
_root.style.top = 0;
_root.style.right = 0;
_root.style.bottom = 0;
_root.style.display = DisplayStyle.None;
_root.style.opacity = 0f;
_root.style.backgroundColor = new Color(0.02f, 0.02f, 0.024f, 0.58f);
_root.style.justifyContent = Justify.Center;
_root.style.alignItems = Align.Center;
_root.RegisterCallback<KeyDownEvent>(HandleRootKeyDown);
_root.RegisterCallback<PointerDownEvent>(evt =>
{
if (_closeOnBackdrop && evt.target == _root)
{
BeginClose();
}
evt.StopPropagation();
});
documentRoot.Add(_root);
_stage = new VisualElement();
_stage.pickingMode = PickingMode.Ignore;
_stage.style.position = Position.Relative;
_stage.style.width = Length.Percent(82f);
_stage.style.maxWidth = 900f;
_stage.style.opacity = 0f;
_stage.style.marginTop = 14f;
_root.Add(_stage);
var shadow = new VisualElement();
shadow.pickingMode = PickingMode.Ignore;
shadow.style.position = Position.Absolute;
shadow.style.left = 0f;
shadow.style.right = 0f;
shadow.style.top = 10f;
shadow.style.bottom = -10f;
shadow.style.backgroundColor = new Color(0f, 0f, 0f, 0.09f);
shadow.style.borderTopLeftRadius = 18f;
shadow.style.borderTopRightRadius = 18f;
shadow.style.borderBottomLeftRadius = 18f;
shadow.style.borderBottomRightRadius = 18f;
_stage.Add(shadow);
_panel = new VisualElement
{
pickingMode = PickingMode.Position
};
_panel.style.width = Length.Percent(100f);
_panel.style.minHeight = 360f;
_panel.style.paddingTop = 36f;
_panel.style.paddingRight = 36f;
_panel.style.paddingBottom = 36f;
_panel.style.paddingLeft = 36f;
_panel.style.backgroundColor = Paper;
_panel.style.borderTopWidth = 1f;
_panel.style.borderRightWidth = 1f;
_panel.style.borderBottomWidth = 1f;
_panel.style.borderLeftWidth = 1f;
_panel.style.borderTopColor = Line;
_panel.style.borderRightColor = Line;
_panel.style.borderBottomColor = Line;
_panel.style.borderLeftColor = Line;
_panel.style.borderTopLeftRadius = 18f;
_panel.style.borderTopRightRadius = 18f;
_panel.style.borderBottomLeftRadius = 18f;
_panel.style.borderBottomRightRadius = 18f;
_panel.style.overflow = Overflow.Hidden;
_stage.Add(_panel);
var accent = new VisualElement();
accent.pickingMode = PickingMode.Ignore;
accent.style.position = Position.Absolute;
accent.style.top = 0f;
accent.style.left = 0f;
accent.style.right = 0f;
accent.style.height = 2f;
accent.style.flexDirection = FlexDirection.Row;
_panel.Add(accent);
AddAccentSegment(accent, Blue);
AddAccentSegment(accent, Teal);
AddAccentSegment(accent, Violet);
_eyebrowLabel = new Label("BRISK SERVICE");
_eyebrowLabel.style.color = Muted;
_eyebrowLabel.style.fontSize = 13f;
_eyebrowLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_eyebrowLabel.style.letterSpacing = 1.4f;
_eyebrowLabel.style.height = 18f;
_eyebrowLabel.style.flexShrink = 0f;
_eyebrowLabel.style.marginBottom = 12f;
_panel.Add(_eyebrowLabel);
_titleLabel = new Label();
_titleLabel.style.color = Ink;
_titleLabel.style.fontSize = 30f;
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_titleLabel.style.whiteSpace = WhiteSpace.Normal;
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
_titleLabel.style.minHeight = 40f;
_titleLabel.style.flexShrink = 0f;
_titleLabel.style.marginBottom = 16f;
_panel.Add(_titleLabel);
_messageLabel = new Label();
_messageLabel.style.color = Muted;
_messageLabel.style.fontSize = 21f;
_messageLabel.style.whiteSpace = WhiteSpace.Normal;
_messageLabel.style.unityTextAlign = TextAnchor.UpperLeft;
_messageLabel.style.minHeight = 64f;
_messageLabel.style.flexShrink = 0f;
_messageLabel.style.marginBottom = 22f;
_panel.Add(_messageLabel);
_spacer = new VisualElement();
_spacer.pickingMode = PickingMode.Ignore;
_spacer.style.flexGrow = 1f;
_spacer.style.minHeight = 18f;
_panel.Add(_spacer);
_divider = new VisualElement();
_divider.style.height = 1f;
_divider.style.flexShrink = 0f;
_divider.style.backgroundColor = Line;
_divider.style.marginBottom = 22f;
_panel.Add(_divider);
_confirmButton = new VisualElement
{
focusable = true,
pickingMode = PickingMode.Position
};
_confirmButton.style.height = 58f;
_confirmButton.style.minHeight = 58f;
_confirmButton.style.flexShrink = 0f;
_confirmButton.style.borderTopLeftRadius = 24f;
_confirmButton.style.borderTopRightRadius = 24f;
_confirmButton.style.borderBottomLeftRadius = 24f;
_confirmButton.style.borderBottomRightRadius = 24f;
_confirmButton.style.backgroundColor = Dark;
_confirmButton.style.justifyContent = Justify.Center;
_confirmButton.style.alignItems = Align.Center;
_confirmButton.RegisterCallback<PointerEnterEvent>(_ => _confirmButton.style.backgroundColor = InkSoft);
_confirmButton.RegisterCallback<PointerLeaveEvent>(_ => _confirmButton.style.backgroundColor = Dark);
_confirmButton.RegisterCallback<PointerDownEvent>(_ =>
{
_confirmButton.style.backgroundColor = Ink;
_confirmButton.style.marginTop = 1f;
});
_confirmButton.RegisterCallback<PointerUpEvent>(_ =>
{
_confirmButton.style.backgroundColor = InkSoft;
_confirmButton.style.marginTop = 0f;
});
_confirmButton.RegisterCallback<ClickEvent>(_ => BeginClose());
_confirmButton.RegisterCallback<KeyDownEvent>(HandleButtonKeyDown);
_panel.Add(_confirmButton);
_confirmLabel = new Label();
_confirmLabel.style.color = Color.white;
_confirmLabel.style.fontSize = 18f;
_confirmLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_confirmLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
_confirmLabel.style.height = 58f;
_confirmButton.Add(_confirmLabel);
}
private void Update()
{
if (_visible && _root != null)
{
ApplySafeAreaPadding();
ApplyResponsiveLayout();
}
}
private void BeginClose()
{
if (!_visible || _root == null)
{
return;
}
StartDialogAnimation(PlayCloseAnimation());
}
private void OnGUI()
{
if (!_visible)
@@ -53,36 +344,379 @@ internal sealed class BriskDefaultErrorDialog : MonoBehaviour
return;
}
var overlayRect = new Rect(0, 0, Screen.width, Screen.height);
DrawLegacyOverlay();
}
private void DrawLegacyOverlay()
{
EnsureLegacyStyles();
var previousDepth = GUI.depth;
var previousMatrix = GUI.matrix;
var previousColor = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(overlayRect, GUIContent.none);
GUI.depth = -10000;
GUI.matrix = Matrix4x4.identity;
DrawLegacyRect(new Rect(0f, 0f, Screen.width, Screen.height), new Color(0.02f, 0.02f, 0.024f, 0.58f));
var scale = GetReadableScale();
ApplyLegacyScale(scale);
var margin = GetDialogMargin(scale);
var padding = GetDialogPadding(scale);
var buttonHeight = GetDialogButtonHeight(scale);
var width = GetDialogWidth(margin);
var contentWidth = width - padding * 2f;
var titleHeight = Mathf.Max(34f, _legacyTitleStyle.CalcHeight(new GUIContent(_titleLabel.text), contentWidth));
var messageHeight = Mathf.Max(48f, _legacyMessageStyle.CalcHeight(new GUIContent(_messageLabel.text), contentWidth));
var minHeight = GetDialogMinHeight(scale, margin);
var contentHeight = padding * 2f + 18f * scale + 12f * scale + titleHeight + 16f * scale + messageHeight + 24f * scale + 1f + 22f * scale + buttonHeight;
var height = Mathf.Clamp(contentHeight, minHeight, Mathf.Max(minHeight, Screen.height - margin * 2f));
var rect = new Rect((Screen.width - width) * 0.5f, (Screen.height - height) * 0.5f, width, height);
DrawLegacyPanel(rect);
var x = rect.x + padding;
var y = rect.y + padding;
GUI.Label(new Rect(x, y, contentWidth, 18f * scale), "BRISK SERVICE", _legacyEyebrowStyle);
y += 30f * scale;
GUI.Label(new Rect(x, y, contentWidth, titleHeight), _titleLabel.text, _legacyTitleStyle);
y += titleHeight + 16f * scale;
GUI.Label(new Rect(x, y, contentWidth, messageHeight), _messageLabel.text, _legacyMessageStyle);
DrawLegacyRect(new Rect(x, rect.yMax - padding - buttonHeight - 22f * scale, contentWidth, 1f), Line);
var buttonRect = new Rect(x, rect.yMax - padding - buttonHeight, contentWidth, buttonHeight);
if (GUI.Button(buttonRect, _confirmLabel.text, _legacyButtonStyle))
{
BeginClose();
}
ConsumeBlockingEvent();
GUI.color = previousColor;
GUI.matrix = previousMatrix;
GUI.depth = previousDepth;
}
var width = Mathf.Min(460f, Screen.width - 40f);
var height = 220f;
var dialogRect = new Rect(
(Screen.width - width) * 0.5f,
(Screen.height - height) * 0.5f,
width,
height);
private IEnumerator PlayOpenAnimation()
{
_root.style.display = DisplayStyle.Flex;
_root.style.opacity = 0f;
_stage.style.opacity = 0f;
_stage.style.marginTop = 14f;
GUILayout.BeginArea(dialogRect, GUI.skin.window);
GUILayout.Space(8f);
GUILayout.Label(_title, GUI.skin.label);
GUILayout.Space(12f);
GUILayout.Label(_message, GUI.skin.label);
GUILayout.FlexibleSpace();
yield return null;
_root.Focus();
if (GUILayout.Button(_confirmText, GUILayout.Height(36f)))
var elapsed = 0f;
while (elapsed < OpenDuration)
{
elapsed += Time.unscaledDeltaTime;
var eased = EaseOutCubic(Mathf.Clamp01(elapsed / OpenDuration));
_root.style.opacity = eased;
_stage.style.opacity = eased;
_stage.style.marginTop = Mathf.Lerp(14f, 0f, eased);
yield return null;
}
_root.style.opacity = 1f;
_stage.style.opacity = 1f;
_stage.style.marginTop = 0f;
}
private IEnumerator PlayCloseAnimation()
{
var callback = _onConfirm;
_visible = false;
_onConfirm = null;
var elapsed = 0f;
while (elapsed < CloseDuration)
{
elapsed += Time.unscaledDeltaTime;
var eased = EaseInCubic(Mathf.Clamp01(elapsed / CloseDuration));
var alpha = 1f - eased;
_root.style.opacity = alpha;
_stage.style.opacity = alpha;
_stage.style.marginTop = Mathf.Lerp(0f, 10f, eased);
yield return null;
}
_root.style.display = DisplayStyle.None;
_visible = false;
callback?.Invoke();
}
GUILayout.Space(8f);
GUILayout.EndArea();
private void StartDialogAnimation(IEnumerator animation)
{
if (_animation != null)
{
StopCoroutine(_animation);
}
_animation = StartCoroutine(animation);
}
private void ApplySafeAreaPadding()
{
var safeArea = Screen.safeArea;
var basePadding = Mathf.Max(20f, Mathf.Min(Screen.width, Screen.height) * 0.04f);
_root.style.paddingLeft = Mathf.Max(basePadding, safeArea.xMin + basePadding);
_root.style.paddingRight = Mathf.Max(basePadding, Screen.width - safeArea.xMax + basePadding);
_root.style.paddingTop = Mathf.Max(basePadding, Screen.height - safeArea.yMax + basePadding);
_root.style.paddingBottom = Mathf.Max(basePadding, safeArea.yMin + basePadding);
}
private void ApplyResponsiveLayout()
{
if (_panel == null)
{
return;
}
var scale = GetReadableScale();
var margin = GetDialogMargin(scale);
var maxWidth = Mathf.Min(900f, Mathf.Max(320f, Screen.width - margin * 2f));
var minHeight = GetDialogMinHeight(scale, margin);
var padding = GetDialogPadding(scale);
var buttonHeight = GetDialogButtonHeight(scale);
_stage.style.width = Length.Percent(82f);
_stage.style.maxWidth = maxWidth;
_panel.style.minHeight = minHeight;
_panel.style.paddingTop = padding;
_panel.style.paddingRight = padding;
_panel.style.paddingBottom = padding;
_panel.style.paddingLeft = padding;
_panel.style.borderTopLeftRadius = 20f * scale;
_panel.style.borderTopRightRadius = 20f * scale;
_panel.style.borderBottomLeftRadius = 20f * scale;
_panel.style.borderBottomRightRadius = 20f * scale;
_eyebrowLabel.style.fontSize = 12f * scale;
_eyebrowLabel.style.height = 18f * scale;
_eyebrowLabel.style.marginBottom = 12f * scale;
_titleLabel.style.fontSize = 28f * scale;
_titleLabel.style.minHeight = 40f * scale;
_titleLabel.style.marginBottom = 16f * scale;
_messageLabel.style.fontSize = 20f * scale;
_messageLabel.style.minHeight = 64f * scale;
_messageLabel.style.marginBottom = 22f * scale;
_spacer.style.minHeight = 18f * scale;
_divider.style.marginBottom = 22f * scale;
_confirmButton.style.height = buttonHeight;
_confirmButton.style.minHeight = buttonHeight;
_confirmButton.style.borderTopLeftRadius = buttonHeight * 0.5f;
_confirmButton.style.borderTopRightRadius = buttonHeight * 0.5f;
_confirmButton.style.borderBottomLeftRadius = buttonHeight * 0.5f;
_confirmButton.style.borderBottomRightRadius = buttonHeight * 0.5f;
_confirmLabel.style.fontSize = 17f * scale;
_confirmLabel.style.height = buttonHeight;
}
private void HandleRootKeyDown(KeyDownEvent evt)
{
if (_closeOnEscape && evt.keyCode == KeyCode.Escape)
{
BeginClose();
evt.StopPropagation();
return;
}
evt.StopPropagation();
}
private void HandleButtonKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter || evt.keyCode == KeyCode.Space)
{
BeginClose();
evt.StopPropagation();
}
}
private void OnDestroy()
{
if (s_instance == this)
{
s_instance = null;
}
if (_panelSettings != null)
{
Destroy(_panelSettings);
_panelSettings = null;
}
}
private void EnsureLegacyStyles()
{
if (_legacyTitleStyle != null)
{
return;
}
_legacyButtonTexture = CreateTexture(Dark);
_legacyButtonHoverTexture = CreateTexture(InkSoft);
_legacyEyebrowStyle = new GUIStyle
{
fontStyle = FontStyle.Bold,
normal = { textColor = Muted }
};
_legacyTitleStyle = new GUIStyle
{
fontStyle = FontStyle.Bold,
wordWrap = true,
normal = { textColor = Ink }
};
_legacyMessageStyle = new GUIStyle
{
wordWrap = true,
normal = { textColor = Muted }
};
_legacyButtonStyle = new GUIStyle
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
border = new RectOffset(24, 24, 24, 24),
normal =
{
background = _legacyButtonTexture,
textColor = Color.white
},
hover =
{
background = _legacyButtonHoverTexture,
textColor = Color.white
},
active =
{
background = _legacyButtonHoverTexture,
textColor = Color.white
}
};
}
private void ApplyLegacyScale(float scale)
{
_legacyEyebrowStyle.fontSize = Mathf.RoundToInt(12f * scale);
_legacyTitleStyle.fontSize = Mathf.RoundToInt(28f * scale);
_legacyMessageStyle.fontSize = Mathf.RoundToInt(20f * scale);
_legacyButtonStyle.fontSize = Mathf.RoundToInt(17f * scale);
}
private void DrawLegacyPanel(Rect rect)
{
DrawLegacyRect(new Rect(rect.x, rect.y + 12f, rect.width, rect.height), new Color(0f, 0f, 0f, 0.08f));
DrawLegacyRect(rect, Paper);
DrawLegacyRect(new Rect(rect.x, rect.y, rect.width, 1f), Line);
DrawLegacyRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), Line);
DrawLegacyRect(new Rect(rect.x, rect.y, 1f, rect.height), Line);
DrawLegacyRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), Line);
var segment = rect.width / 3f;
DrawLegacyRect(new Rect(rect.x, rect.y, segment, 2f), Blue);
DrawLegacyRect(new Rect(rect.x + segment, rect.y, segment, 2f), Teal);
DrawLegacyRect(new Rect(rect.x + segment * 2f, rect.y, rect.width - segment * 2f, 2f), Violet);
}
private static void ConsumeBlockingEvent()
{
var evt = Event.current;
if (evt == null)
{
return;
}
switch (evt.type)
{
case EventType.MouseDown:
case EventType.MouseUp:
case EventType.MouseDrag:
case EventType.ScrollWheel:
case EventType.KeyDown:
case EventType.KeyUp:
evt.Use();
break;
}
}
private static Texture2D CreateTexture(Color color)
{
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false)
{
hideFlags = HideFlags.HideAndDontSave
};
texture.SetPixel(0, 0, color);
texture.Apply();
return texture;
}
private static void DrawLegacyRect(Rect rect, Color color)
{
var previousColor = GUI.color;
GUI.color = color;
GUI.DrawTexture(rect, Texture2D.whiteTexture);
GUI.color = previousColor;
}
private static void AddAccentSegment(VisualElement parent, Color color)
{
var segment = new VisualElement();
segment.style.flexGrow = 1f;
segment.style.backgroundColor = color;
parent.Add(segment);
}
private static float GetReadableScale()
{
return Mathf.Clamp(Mathf.Min(Screen.width, Screen.height) / 720f, 1f, 1.22f);
}
private static float GetDialogMargin(float scale)
{
return Mathf.Max(24f * scale, Mathf.Min(Screen.width, Screen.height) * 0.06f);
}
private static float GetDialogPadding(float scale)
{
return 34f * scale;
}
private static float GetDialogButtonHeight(float scale)
{
return 56f * scale;
}
private static float GetDialogWidth(float margin)
{
return Mathf.Clamp(Screen.width * 0.82f, 320f, Mathf.Max(320f, Mathf.Min(900f, Screen.width - margin * 2f)));
}
private static float GetDialogMinHeight(float scale, float margin)
{
var availableHeight = Mathf.Max(320f, Screen.height - margin * 2f);
return Mathf.Min(availableHeight, Mathf.Clamp(Screen.height * 0.24f, 360f * scale, 620f));
}
private static float EaseOutCubic(float t)
{
return 1f - Mathf.Pow(1f - t, 3f);
}
private static float EaseInCubic(float t)
{
return t * t * t;
}
private static Color ColorFromHex(int rgb)
{
var r = ((rgb >> 16) & 0xff) / 255f;
var g = ((rgb >> 8) & 0xff) / 255f;
var b = (rgb & 0xff) / 255f;
return new Color(r, g, b, 1f);
}
}

View File

@@ -13,18 +13,22 @@ public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
public void ShowBlockingError(BriskBlockingException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "A blocking error occurred.";
var message = GetBlockingMessage(exception);
BriskDefaultErrorDialog.Show(
title: GetBlockingTitle(exception),
message: message,
confirmText: "OK",
confirmText: "知道了",
onConfirm: () =>
{
var exitHandler = Brisk.IsInitialized ? Brisk.Options?.ExitHandler : null;
exitHandler?.Invoke();
var exitHandler = Brisk.IsInitialized ? Brisk.GetRequiredContext().ExitHandler : null;
if (exitHandler != null)
{
exitHandler.Invoke();
return;
}
Application.Quit();
});
Debug.LogError("[Brisk] Blocking error: " + message);
@@ -32,35 +36,63 @@ public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
public void ShowAuthExpired(BriskAuthExpiredException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "Login expired. Please sign in again.";
var message = GetAuthExpiredMessage(exception);
BriskDefaultErrorDialog.Show(
title: "Login Expired",
title: "登录已过期",
message: message,
confirmText: "OK");
confirmText: "知道了");
Debug.LogWarning("[Brisk] Auth expired: " + message);
Debug.LogWarning("[Brisk] Auth expired: " + (exception != null ? exception.Message : message));
}
private static string GetBlockingTitle(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "Maintenance";
return "服务维护中";
}
if (exception is BriskAccountBannedException)
{
return "Account Restricted";
return "账号受限";
}
if (exception is BriskClientUpdateRequiredException)
{
return "Update Required";
return "需要更新";
}
return "Service Unavailable";
return "服务暂不可用";
}
private static string GetBlockingMessage(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "服务正在维护,请稍后再试。";
}
if (exception is BriskAccountBannedException)
{
return "当前账号暂时无法使用该服务。";
}
if (exception is BriskClientUpdateRequiredException)
{
return "当前版本过低,请更新后继续。";
}
return "服务暂时不可用,请稍后再试。";
}
private static string GetAuthExpiredMessage(BriskAuthExpiredException exception)
{
if (exception != null && exception.Message != null && exception.Message.IndexOf("missing", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "当前登录状态不可用,请重新登录。";
}
return "登录状态已失效,请重新登录后继续。";
}
}

View File

@@ -249,8 +249,10 @@ internal static class BriskModelMapper
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -276,7 +278,9 @@ internal static class BriskModelMapper
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -309,7 +313,11 @@ internal static class BriskModelMapper
return new BriskSpaceLikeResult
{
Liked = BriskValueReader.GetBool(data, "liked"),
LikeCount = BriskValueReader.GetLong(data, "like_count")
Created = BriskValueReader.GetBool(data, "created"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
};
}

View File

@@ -30,6 +30,10 @@ public sealed class BriskOptions
/// </summary>
public bool ValidateSessionOnInitialize = true;
/// <summary>
/// 初始化恢复的本地会话已过期或失效时,是否使用本地保存的登录身份静默换取新会话。
/// </summary>
public bool AutoReloginOnInitialize = true;
/// <summary>
/// 自定义登录态持久化实现。
/// </summary>
public IBriskTokenStore TokenStore;

View File

@@ -1,5 +1,9 @@
public sealed class BriskSpaceLikeResult
{
public bool Liked;
public bool Created;
public bool LikedByMe;
public long LikeCount;
public long TodayLikeCount;
public string LikeResetAt;
}

View File

@@ -12,6 +12,8 @@ public sealed class BriskSpaceStats
public long ContentSizeBytes;
public string ContentChecksum;
public long LikeCount;
public long TodayLikeCount;
public long VisitCount;
public string LikeResetAt;
public string UpdatedAt;
}

View File

@@ -12,7 +12,9 @@ public sealed class BriskSpaceView
public long ContentSizeBytes;
public string ContentChecksum;
public long LikeCount;
public long TodayLikeCount;
public long VisitCount;
public bool LikedByMe;
public string LikeResetAt;
public string UpdatedAt;
}

View File

@@ -68,23 +68,23 @@ public sealed class BriskSpaceModule
}
/// <summary>
/// 按玩家 ID 获取最近点赞列表。
/// 按玩家 ID 获取点赞列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20)
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20, bool currentCycleOnly = false)
{
ValidatePlayerId(playerId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLimitQuery(limit), true);
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLikesQuery(limit, currentCycleOnly), true);
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
});
}
/// <summary>
/// 按登录身份获取最近点赞列表。
/// 按登录身份获取点赞列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20)
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20, bool currentCycleOnly = false)
{
ValidateLoginIdentity(loginProvider, loginUserId);
@@ -92,6 +92,10 @@ public sealed class BriskSpaceModule
{
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
query["limit"] = NormalizeLimit(limit);
if (currentCycleOnly)
{
query["scope"] = "cycle";
}
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
});
@@ -236,7 +240,7 @@ public sealed class BriskSpaceModule
/// <summary>
/// 获取我的访客列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 20)
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 50)
{
return await ExecuteAsync(async context =>
{
@@ -262,6 +266,17 @@ public sealed class BriskSpaceModule
};
}
private static Dictionary<string, string> CreateLikesQuery(int limit, bool currentCycleOnly)
{
var query = CreateLimitQuery(limit);
if (currentCycleOnly)
{
query["scope"] = "cycle";
}
return query;
}
private static void ValidatePlayerId(string playerId)
{
RequireNotEmpty(playerId, nameof(playerId));

View File

@@ -112,6 +112,8 @@ public sealed class BriskQuickStartSample : MonoBehaviour
private GUIStyle _statusStyle;
private GUIStyle _listButtonStyle;
private GUIStyle _modalStyle;
private PropertyInfo _defaultDialogVisibleProperty;
private MethodInfo _defaultDialogRenderMethod;
private void OnEnable()
{
@@ -142,6 +144,12 @@ public sealed class BriskQuickStartSample : MonoBehaviour
private void OnGUI()
{
if (ShouldRouteInputToDefaultDialog())
{
RenderDefaultErrorDialogTopmost();
return;
}
EnsureStyles();
var scale = Mathf.Min(Screen.width / DesignWidth, Screen.height / DesignHeight);
@@ -167,6 +175,54 @@ public sealed class BriskQuickStartSample : MonoBehaviour
}
GUI.matrix = previousMatrix;
RenderDefaultErrorDialogTopmost();
}
private bool ShouldRouteInputToDefaultDialog()
{
if (!IsDefaultErrorDialogVisible() || Event.current == null)
{
return false;
}
switch (Event.current.type)
{
case EventType.Layout:
case EventType.Repaint:
return false;
default:
return true;
}
}
private bool IsDefaultErrorDialogVisible()
{
EnsureDefaultDialogReflection();
return _defaultDialogVisibleProperty != null && (bool)_defaultDialogVisibleProperty.GetValue(null, null);
}
private void RenderDefaultErrorDialogTopmost()
{
EnsureDefaultDialogReflection();
_defaultDialogRenderMethod?.Invoke(null, null);
}
private void EnsureDefaultDialogReflection()
{
if (_defaultDialogVisibleProperty != null && _defaultDialogRenderMethod != null)
{
return;
}
var dialogType = typeof(Brisk).Assembly.GetType("BriskDefaultErrorDialog");
if (dialogType == null)
{
return;
}
_defaultDialogVisibleProperty = dialogType.GetProperty("IsVisible", BindingFlags.Static | BindingFlags.NonPublic);
_defaultDialogRenderMethod = dialogType.GetMethod("RenderTopmostLegacyOverlay", BindingFlags.Static | BindingFlags.NonPublic);
}
private void DrawCurrentPage()
@@ -219,6 +275,19 @@ public sealed class BriskQuickStartSample : MonoBehaviour
GUILayout.Label("失败会停留在当前页,并把错误展示在底部状态栏。", _bodyStyle);
});
DrawCard("默认弹窗外观预览", () =>
{
GUILayout.Label("这些按钮只展示 SDK 默认弹窗外观,不会修改登录状态,也不会请求服务器。", _bodyStyle);
DrawInlineButtons(
new ButtonSpec("登录过期", PreviewAuthExpiredDialogAsync, false, true),
new ButtonSpec("缺少 Token", PreviewMissingTokenDialogAsync, false, true));
GUILayout.Space(8f);
DrawInlineButtons(
new ButtonSpec("服务维护", PreviewMaintenanceDialogAsync, false, true),
new ButtonSpec("账号受限", PreviewAccountBannedDialogAsync, false, true),
new ButtonSpec("需要更新", PreviewUpdateRequiredDialogAsync, false, true));
});
GUILayout.FlexibleSpace();
GUILayout.EndScrollView();
GUILayout.EndArea();
@@ -475,14 +544,16 @@ public sealed class BriskQuickStartSample : MonoBehaviour
DrawValueRow("内容版本", _mySpaceView == null ? "-" : _mySpaceView.ContentVersion.ToString());
DrawValueRow("内容类型", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.ContentType));
DrawValueRow("内容大小", _mySpaceView == null ? "-" : _mySpaceView.ContentSizeBytes + " bytes");
DrawValueRow("点赞数", _mySpaceStats == null ? "-" : _mySpaceStats.LikeCount.ToString());
DrawValueRow("累计点赞数", _mySpaceStats == null ? "-" : _mySpaceStats.LikeCount.ToString());
DrawValueRow("今日点赞数", _mySpaceStats == null ? "-" : _mySpaceStats.TodayLikeCount.ToString());
DrawValueRow("访问数", _mySpaceStats == null ? "-" : _mySpaceStats.VisitCount.ToString());
DrawValueRow("今日点赞重置", _mySpaceStats == null ? "-" : FormatLikeResetAt(_mySpaceStats.LikeResetAt));
DrawValueRow("更新时间", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.UpdatedAt));
GUILayout.Space(8f);
GUILayout.Label("最近给我点赞的用户", _sectionTitleStyle);
GUILayout.Label("今天给我点赞的用户", _sectionTitleStyle);
if (_mySpaceLikes.Count == 0)
{
GUILayout.Label("暂无点赞记录。", _bodyStyle);
GUILayout.Label("当前周期暂无点赞记录。", _bodyStyle);
}
else
{
@@ -509,9 +580,11 @@ public sealed class BriskQuickStartSample : MonoBehaviour
DrawValueRow("PlayerId", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.PlayerId));
DrawValueRow("内容类型", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.ContentType));
DrawValueRow("内容大小", _targetSpaceView == null ? "-" : _targetSpaceView.ContentSizeBytes + " bytes");
DrawValueRow("点赞数", _targetSpaceStats == null ? "-" : _targetSpaceStats.LikeCount.ToString());
DrawValueRow("累计点赞数", _targetSpaceStats == null ? "-" : _targetSpaceStats.LikeCount.ToString());
DrawValueRow("今日点赞数", _targetSpaceStats == null ? "-" : _targetSpaceStats.TodayLikeCount.ToString());
DrawValueRow("访问数", _targetSpaceStats == null ? "-" : _targetSpaceStats.VisitCount.ToString());
DrawValueRow("我的点赞状态", _targetSpaceView != null && _targetSpaceView.LikedByMe ? "已点赞" : "未点赞");
DrawValueRow("今日点赞状态", _targetSpaceView != null && _targetSpaceView.LikedByMe ? "今日已点赞" : "今日未点赞");
DrawValueRow("今日点赞重置", _targetSpaceView == null ? "-" : FormatLikeResetAt(_targetSpaceView.LikeResetAt));
GUILayout.Space(8f);
GUILayout.Label("空间内容", _sectionTitleStyle);
GUILayout.TextArea(_targetSpaceContentText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f));
@@ -520,8 +593,8 @@ public sealed class BriskQuickStartSample : MonoBehaviour
DrawCard("操作", () =>
{
DrawInlineButtons(
new ButtonSpec("点赞", LikeTargetSpaceAsync, false, true),
new ButtonSpec("取消点赞", UnlikeTargetSpaceAsync, false, true));
new ButtonSpec("今日点赞", LikeTargetSpaceAsync, false, _targetSpaceView != null && !_targetSpaceView.LikedByMe),
new ButtonSpec("撤销今日点赞", UnlikeTargetSpaceAsync, false, _targetSpaceView != null && _targetSpaceView.LikedByMe));
});
}
@@ -559,6 +632,36 @@ public sealed class BriskQuickStartSample : MonoBehaviour
_page = SamplePage.Home;
}
private Task PreviewAuthExpiredDialogAsync()
{
BriskDefaultErrorPresenter.Instance.ShowAuthExpired(new BriskAuthExpiredException("invalid token"));
return Task.CompletedTask;
}
private Task PreviewMissingTokenDialogAsync()
{
BriskDefaultErrorPresenter.Instance.ShowAuthExpired(new BriskAuthExpiredException("Missing access token."));
return Task.CompletedTask;
}
private Task PreviewMaintenanceDialogAsync()
{
BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskMaintenanceException("maintenance"));
return Task.CompletedTask;
}
private Task PreviewAccountBannedDialogAsync()
{
BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskAccountBannedException("account banned"));
return Task.CompletedTask;
}
private Task PreviewUpdateRequiredDialogAsync()
{
BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskClientUpdateRequiredException("client update required"));
return Task.CompletedTask;
}
private async Task RefreshHomeAsync()
{
_me = await Brisk.Player.GetMeAsync();
@@ -636,7 +739,7 @@ public sealed class BriskQuickStartSample : MonoBehaviour
_mySpaceView = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId);
_mySpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(Brisk.PlayerId);
_mySpaceLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 10);
_mySpaceLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 10, true);
SpacePayloadText = await DownloadSpaceTextAsync(_mySpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId));
}
@@ -673,13 +776,15 @@ public sealed class BriskQuickStartSample : MonoBehaviour
private async Task LikeTargetSpaceAsync()
{
await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId);
var result = await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId);
Log("空间今日点赞结果: created=" + result.Created + ", total=" + result.LikeCount + ", today=" + result.TodayLikeCount);
await LoadTargetSpaceAsync();
}
private async Task UnlikeTargetSpaceAsync()
{
await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId);
var result = await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId);
Log("空间撤销今日点赞结果: total=" + result.LikeCount + ", today=" + result.TodayLikeCount);
await LoadTargetSpaceAsync();
}
@@ -1120,6 +1225,22 @@ public sealed class BriskQuickStartSample : MonoBehaviour
return string.IsNullOrWhiteSpace(value) ? "-" : value;
}
private static string FormatLikeResetAt(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "-";
}
DateTimeOffset parsed;
if (!DateTimeOffset.TryParse(value, out parsed))
{
return value;
}
return parsed.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
private static async Task<string> DownloadSpaceTextAsync(BriskSpaceView view, Func<Task<BriskSpaceContentDownloadResult>> downloadContent)
{
if (view == null || !view.ContentExists || downloadContent == null)

View File

@@ -259,7 +259,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
BaseUrl: https://brisk.lightyears.ltd
GameKey: briskh5verify
GameKey: game_a1faf5ee93d0
ClientVersion: 1.0.0
DeviceId: editor-device
ValidateSessionOnInitialize: 1
@@ -268,7 +268,7 @@ MonoBehaviour:
LoginCode:
Nickname: "Unity\u793A\u4F8B\u73A9\u5BB6"
AvatarUrl:
RankKey: h5-verify-rank-20260410034312-5cdd
RankKey: rank-20260411062004-2bce
SubmitScoreValue: 128
LeaderboardLimit: 10
AroundMeRange: 5

View File

@@ -1,16 +0,0 @@
# Changelog
## 0.2.0
- Refined archive APIs with direct text and JSON upload/download helpers
- Refactored player space to the latest metadata plus binary content architecture
- Added space content download/update result models and like list models
- Updated quick start sample to use the latest archive and space flows
- Updated package and integration docs to match the current API surface
## 0.1.0
- Initial embedded package structure
- Added Brisk runtime facade and service modules
- Added default error presenter
- Added quick start sample script and sample scene

View File

@@ -1,68 +0,0 @@
# FoldCC Brisk Game Server SDK
This directory is the package publishing skeleton for Brisk.
The active Unity project source currently lives under:
- `Assets/BriskSdk/Runtime`
- `Assets/BriskSdk/Samples/QuickStart`
- `Assets/Scenes/BriskQuickStartScene.unity`
Sync package content from the Unity source project with:
- `Tools/Sync-BriskPackage.ps1`
## Included runtime modules
- Bootstrap and initialization
- Auth and session restore
- Player profile
- Dynamic config
- Announcements
- Leaderboard
- Archive upload and download
- Player space
- Default blocking error UI
## Archive checksum
Archive upload checksum is handled by the SDK by default.
- For quick use, the archive module now provides:
- `UploadAsync(slotNo, bytes)` for binary
- `UploadTextAsync(slotNo, text)` for UTF-8 text
- `UploadJsonAsync(slotNo, payload)` for JSON objects
- `DownloadAsync(slotNo)` for raw bytes + metadata
- `DownloadTextAsync(slotNo)` for UTF-8 text
- `DownloadJsonAsync(slotNo)` for JSON payloads
- The SDK computes SHA256 automatically when uploading archive bytes
- The current Brisk archive API expects a plain lowercase SHA256 hex string
- Do not send values with a `sha256:` prefix
- If a manual checksum includes that prefix, the SDK will normalize it before sending
## Space content
Player space now follows a metadata + binary content model.
- `GetByPlayerIdAsync(...)` and `GetByLoginIdentityAsync(...)` return metadata only
- `DownloadContentByPlayerIdAsync(...)` and `DownloadContentByLoginIdentityAsync(...)` return raw bytes
- `UpdateMyAsync(string)` uploads text content directly
- `UpdateMyAsync(byte[])` uploads binary content directly
- `UpdateMyAsync(object)` serializes the object as JSON automatically
## Package layout
- `Runtime`
- `Samples~`
- `Documentation~`
## Quick start
See:
- `Documentation~/QuickStart.md`
- `Samples~/QuickStart`
This sample uses an IMGUI test panel for end-to-end SDK flow verification.
When preparing a publish branch, place the package-ready runtime, samples, and docs into this package directory and tag that branch for external consumption.

View File

@@ -1,88 +0,0 @@
using System;
using UnityEngine;
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
{
private const string HostName = "BriskDefaultErrorDialog";
private static BriskDefaultErrorDialog s_instance;
private string _title;
private string _message;
private string _confirmText;
private Action _onConfirm;
private bool _visible;
public static void Show(string title, string message, string confirmText, Action onConfirm = null)
{
var host = EnsureInstance();
host._title = string.IsNullOrWhiteSpace(title) ? "Brisk" : title;
host._message = string.IsNullOrWhiteSpace(message) ? "Unknown error." : message;
host._confirmText = string.IsNullOrWhiteSpace(confirmText) ? "OK" : confirmText;
host._onConfirm = onConfirm;
host._visible = true;
}
private static BriskDefaultErrorDialog EnsureInstance()
{
if (s_instance != null)
{
return s_instance;
}
var existing = GameObject.Find(HostName);
if (existing != null)
{
s_instance = existing.GetComponent<BriskDefaultErrorDialog>();
if (s_instance != null)
{
return s_instance;
}
}
var gameObject = new GameObject(HostName);
DontDestroyOnLoad(gameObject);
s_instance = gameObject.AddComponent<BriskDefaultErrorDialog>();
return s_instance;
}
private void OnGUI()
{
if (!_visible)
{
return;
}
var overlayRect = new Rect(0, 0, Screen.width, Screen.height);
var previousColor = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(overlayRect, GUIContent.none);
GUI.color = previousColor;
var width = Mathf.Min(460f, Screen.width - 40f);
var height = 220f;
var dialogRect = new Rect(
(Screen.width - width) * 0.5f,
(Screen.height - height) * 0.5f,
width,
height);
GUILayout.BeginArea(dialogRect, GUI.skin.window);
GUILayout.Space(8f);
GUILayout.Label(_title, GUI.skin.label);
GUILayout.Space(12f);
GUILayout.Label(_message, GUI.skin.label);
GUILayout.FlexibleSpace();
if (GUILayout.Button(_confirmText, GUILayout.Height(36f)))
{
var callback = _onConfirm;
_visible = false;
_onConfirm = null;
callback?.Invoke();
}
GUILayout.Space(8f);
GUILayout.EndArea();
}
}

View File

@@ -1,66 +0,0 @@
using System;
using UnityEngine;
public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
{
private static readonly BriskDefaultErrorPresenter InstanceValue = new BriskDefaultErrorPresenter();
private BriskDefaultErrorPresenter()
{
}
public static BriskDefaultErrorPresenter Instance => InstanceValue;
public void ShowBlockingError(BriskBlockingException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "A blocking error occurred.";
BriskDefaultErrorDialog.Show(
title: GetBlockingTitle(exception),
message: message,
confirmText: "OK",
onConfirm: () =>
{
var exitHandler = Brisk.IsInitialized ? Brisk.Options?.ExitHandler : null;
exitHandler?.Invoke();
});
Debug.LogError("[Brisk] Blocking error: " + message);
}
public void ShowAuthExpired(BriskAuthExpiredException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "Login expired. Please sign in again.";
BriskDefaultErrorDialog.Show(
title: "Login Expired",
message: message,
confirmText: "OK");
Debug.LogWarning("[Brisk] Auth expired: " + message);
}
private static string GetBlockingTitle(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "Maintenance";
}
if (exception is BriskAccountBannedException)
{
return "Account Restricted";
}
if (exception is BriskClientUpdateRequiredException)
{
return "Update Required";
}
return "Service Unavailable";
}
}

View File

@@ -1,5 +0,0 @@
public sealed class BriskSpaceLikeResult
{
public bool Liked;
public long LikeCount;
}

View File

@@ -0,0 +1,38 @@
# Changelog
## 0.4.1
- Startup restore now silently clears locally expired or server-rejected sessions without showing the default auth-expired dialog
- Added silent initialization relogin from the stored `login_provider` and `login_user_id` when available
- Localized the default blocking/auth-expired dialogs to Chinese and stopped showing raw server messages to players
- Rebuilt the default dialog with a larger mobile-readable Runtime UI Toolkit layout and topmost legacy IMGUI bridge
- Default dialogs no longer close from backdrop or Escape; blocking dialogs now invoke the configured exit handler or fall back to `Application.Quit()`
## 0.4.0
- Added daily-cycle space like fields and result semantics to the Unity SDK models
- Added current-cycle filtering support for space like list APIs
- Updated the quick start sample to display total likes, today likes, like reset time, and current-cycle like actions
- Changed `GetMyVisitsAsync()` default page size to `50` while preserving the optional `limit` parameter
- Refreshed package and integration documentation to match the latest production API behavior
## 0.3.0
- Renamed the UPM package identifier to `com.foldcc.cc-framework.brisk-game-server`
- Renamed the publish directory to the all-lowercase package path
- Updated sync tooling and integration documentation for Git-based package import
## 0.2.0
- Refined archive APIs with direct text and JSON upload/download helpers
- Refactored player space to the latest metadata plus binary content architecture
- Added space content download/update result models and like list models
- Updated quick start sample to use the latest archive and space flows
- Updated package and integration docs to match the current API surface
## 0.1.0
- Initial embedded package structure
- Added Brisk runtime facade and service modules
- Added default error presenter
- Added quick start sample script and sample scene

View File

@@ -64,6 +64,12 @@ await Brisk.Space.UpdateMyAsync(new
var mySpace = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId);
var myContent = await Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId);
var text = Encoding.UTF8.GetString(myContent.Bytes);
var likeResult = await Brisk.Space.LikeByPlayerIdAsync("target-player-id");
Debug.Log($"total={likeResult.LikeCount}, today={likeResult.TodayLikeCount}, created={likeResult.Created}");
var todayLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 20, true);
var visits = await Brisk.Space.GetMyVisitsAsync();
```
Notes:
@@ -72,6 +78,11 @@ Notes:
- `GetByPlayerIdAsync(...)` returns metadata only
- use `DownloadContentByPlayerIdAsync(...)` to read the actual content bytes
- `UpdateMyAsync(...)` automatically picks text / binary / json behavior from the payload type
- space likes are now tracked by natural day in `Asia/Shanghai`
- `BriskSpaceView` / `BriskSpaceStats` include both `LikeCount` and `TodayLikeCount`
- `BriskSpaceView` also includes `LikedByMe` and `LikeResetAt`
- `GetLikesByPlayerIdAsync(..., currentCycleOnly: true)` returns current-cycle likes only
- `GetMyVisitsAsync()` now defaults to the latest `50` visits, with `100` as the server-side max
## Sample

View File

@@ -0,0 +1,105 @@
# FoldCC Brisk Game Server SDK
Brisk Unity SDK 的 UPM 发布目录。
当前仓库地址:
- `http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer`
当前包名:
- `com.foldcc.cc-framework.brisk-game-server`
## 如何引入到项目
推荐通过 Unity Package Manager 的 Git URL 方式引入。
### 方式一Package Manager
1. 打开 `Window > Package Manager`
2. 点击左上角 `+`
3. 选择 `Add package from git URL...`
4. 输入:
```text
http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.4.1
```
### 方式二:修改 `Packages/manifest.json`
```json
{
"dependencies": {
"com.foldcc.cc-framework.brisk-game-server": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.4.1"
}
}
```
如果需要跟随主分支最新代码,可将末尾的 `#v0.4.1` 改成 `#main`;正式环境建议固定到发布 tag。
## 开发态源码位置
开发工程中的活代码位于:
- `Assets/BriskSdk/Runtime`
- `Assets/BriskSdk/Samples/QuickStart`
- `Assets/Scenes/BriskQuickStartScene.unity`
同步 package 内容时执行:
```powershell
./Tools/Sync-BriskPackage.ps1
```
## 已包含模块
- Bootstrap 与初始化
- Auth 与会话恢复
- 玩家信息
- 动态配置
- 公告
- 排行榜
- 云存档上传下载
- 玩家空间
- 默认阻断式错误 UI
## Archive checksum
云存档上传时SDK 默认自动处理 checksum。
- `UploadAsync(slotNo, bytes)` 用于二进制
- `UploadTextAsync(slotNo, text)` 用于 UTF-8 文本
- `UploadJsonAsync(slotNo, payload)` 用于 JSON 对象
- `DownloadAsync(slotNo)` 返回原始 bytes 和元信息
- `DownloadTextAsync(slotNo)` 返回 UTF-8 文本
- `DownloadJsonAsync(slotNo)` 返回 JSON 结果
- SDK 会自动计算 SHA256
- 当前 Brisk archive API 要求纯小写 SHA256 十六进制字符串
- 不要传 `sha256:` 前缀
- 若手动传入带前缀的 checksumSDK 会自动归一化
## Space content
玩家空间当前采用 metadata + binary content 模型。
- `GetByPlayerIdAsync(...)``GetByLoginIdentityAsync(...)` 返回元数据
- `DownloadContentByPlayerIdAsync(...)``DownloadContentByLoginIdentityAsync(...)` 返回原始 bytes
- `UpdateMyAsync(string)` 直接上传文本
- `UpdateMyAsync(byte[])` 直接上传二进制
- `UpdateMyAsync(object)` 自动序列化为 JSON
- `LikeByPlayerIdAsync(...)` / `UnlikeByPlayerIdAsync(...)` 返回累计点赞数、今日点赞数、当前周期是否创建新点赞、重置时间
- `GetLikesByPlayerIdAsync(playerId, limit, true)``GetLikesByLoginIdentityAsync(..., true)` 可只读取当前周期点赞记录
- `GetMyVisitsAsync()` 默认读取最近 `50` 条访问记录,也可手动传入 `limit`
## 目录结构
- `Runtime`
- `Samples~`
- `Documentation~`
## 快速开始
查看:
- `Documentation~/QuickStart.md`
- `Samples~/QuickStart`

View File

@@ -197,8 +197,17 @@ public static class Brisk
}
internal static void NotifyAuthExpired(BriskAuthExpiredException exception)
{
NotifyAuthExpired(exception, true);
}
internal static void NotifyAuthExpired(BriskAuthExpiredException exception, bool showDefaultPresenter)
{
if (showDefaultPresenter)
{
s_context?.ErrorPresenter?.ShowAuthExpired(exception);
}
OnAuthExpired?.Invoke(exception);
}
@@ -241,6 +250,15 @@ public static class Brisk
return;
}
if (storedSession.ExpiresAt.HasValue && storedSession.ExpiresAt.Value <= DateTimeOffset.UtcNow)
{
await HandleExpiredStartupSessionAsync(
context,
storedSession,
new BriskAuthExpiredException("Stored session expired before initialization."));
return;
}
context.Session.Update(
storedSession.AccessToken,
storedSession.ExpiresAt,
@@ -268,9 +286,7 @@ public static class Brisk
}
catch (BriskAuthExpiredException exception)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
NotifyAuthExpired(exception);
await HandleExpiredStartupSessionAsync(context, storedSession, exception);
}
catch (BriskBlockingException exception)
{
@@ -278,4 +294,36 @@ public static class Brisk
throw;
}
}
private static async Task HandleExpiredStartupSessionAsync(BriskContext context, BriskStoredSession storedSession, BriskAuthExpiredException exception)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
NotifyAuthExpired(exception, false);
if (!context.Options.AutoReloginOnInitialize || !HasStoredLoginIdentity(storedSession))
{
return;
}
try
{
await Auth.LoginWithUserIdAsync(storedSession.LoginProvider, storedSession.LoginUserId);
}
catch (BriskBlockingException)
{
throw;
}
catch
{
// 初始化期静默重登失败时,交给业务层后续第三方登录流程重新调用 Brisk.Auth。
}
}
private static bool HasStoredLoginIdentity(BriskStoredSession storedSession)
{
return storedSession != null
&& !string.IsNullOrWhiteSpace(storedSession.LoginProvider)
&& !string.IsNullOrWhiteSpace(storedSession.LoginUserId);
}
}

View File

@@ -0,0 +1,722 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;
[DefaultExecutionOrder(10000)]
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
{
private const string HostName = "BriskDefaultErrorDialog";
private const float OpenDuration = 0.24f;
private const float CloseDuration = 0.2f;
private static readonly Color Ink = ColorFromHex(0x05060a);
private static readonly Color InkSoft = ColorFromHex(0x1b1d24);
private static readonly Color Muted = ColorFromHex(0x5f6572);
private static readonly Color Line = ColorFromHex(0xdde2ea);
private static readonly Color Paper = ColorFromHex(0xffffff);
private static readonly Color Soft = ColorFromHex(0xf7f8fb);
private static readonly Color Blue = ColorFromHex(0x2563ff);
private static readonly Color Teal = ColorFromHex(0x23d6c8);
private static readonly Color Violet = ColorFromHex(0x8b5cff);
private static readonly Color Dark = ColorFromHex(0x050506);
private static BriskDefaultErrorDialog s_instance;
private UIDocument _document;
private PanelSettings _panelSettings;
private VisualElement _root;
private VisualElement _stage;
private VisualElement _panel;
private Label _eyebrowLabel;
private Label _titleLabel;
private Label _messageLabel;
private VisualElement _spacer;
private VisualElement _divider;
private VisualElement _confirmButton;
private Label _confirmLabel;
private Action _onConfirm;
private Coroutine _animation;
private bool _visible;
private bool _closeOnBackdrop;
private bool _closeOnEscape;
private GUIStyle _legacyTitleStyle;
private GUIStyle _legacyMessageStyle;
private GUIStyle _legacyEyebrowStyle;
private GUIStyle _legacyButtonStyle;
private Texture2D _legacyButtonTexture;
private Texture2D _legacyButtonHoverTexture;
internal static bool IsVisible => s_instance != null && s_instance._visible;
internal static void RenderTopmostLegacyOverlay()
{
if (s_instance == null || !s_instance._visible)
{
return;
}
s_instance.DrawLegacyOverlay();
}
public static void Show(string title, string message, string confirmText, Action onConfirm = null, bool closeOnBackdrop = false, bool closeOnEscape = false)
{
var host = EnsureInstance();
host.ShowInternal(
string.IsNullOrWhiteSpace(title) ? "Brisk" : title,
string.IsNullOrWhiteSpace(message) ? "服务暂时不可用,请稍后再试。" : message,
string.IsNullOrWhiteSpace(confirmText) ? "知道了" : confirmText,
onConfirm,
closeOnBackdrop,
closeOnEscape);
}
private static BriskDefaultErrorDialog EnsureInstance()
{
if (s_instance != null)
{
return s_instance;
}
var existing = GameObject.Find(HostName);
if (existing != null)
{
s_instance = existing.GetComponent<BriskDefaultErrorDialog>();
if (s_instance != null)
{
return s_instance;
}
}
var gameObject = new GameObject(HostName);
DontDestroyOnLoad(gameObject);
s_instance = gameObject.AddComponent<BriskDefaultErrorDialog>();
return s_instance;
}
private void ShowInternal(string title, string message, string confirmText, Action onConfirm, bool closeOnBackdrop, bool closeOnEscape)
{
EnsureDocument();
ApplySafeAreaPadding();
_titleLabel.text = title;
_messageLabel.text = message;
_confirmLabel.text = confirmText;
_onConfirm = onConfirm;
_closeOnBackdrop = closeOnBackdrop;
_closeOnEscape = closeOnEscape;
_visible = true;
StartDialogAnimation(PlayOpenAnimation());
}
private void EnsureDocument()
{
if (_document != null)
{
ApplyResponsiveLayout();
return;
}
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
_panelSettings.name = HostName + "PanelSettings";
_panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
_panelSettings.scale = 1f;
_panelSettings.sortingOrder = 32767f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = _panelSettings;
_document.sortingOrder = 32767f;
BuildVisualTree(_document.rootVisualElement);
ApplyResponsiveLayout();
}
private void BuildVisualTree(VisualElement documentRoot)
{
documentRoot.Clear();
documentRoot.pickingMode = PickingMode.Ignore;
documentRoot.style.position = Position.Absolute;
documentRoot.style.left = 0f;
documentRoot.style.top = 0f;
documentRoot.style.right = 0f;
documentRoot.style.bottom = 0f;
documentRoot.style.width = Length.Percent(100f);
documentRoot.style.height = Length.Percent(100f);
documentRoot.style.flexGrow = 1f;
_root = new VisualElement
{
pickingMode = PickingMode.Position,
focusable = true
};
_root.style.position = Position.Absolute;
_root.style.left = 0;
_root.style.top = 0;
_root.style.right = 0;
_root.style.bottom = 0;
_root.style.display = DisplayStyle.None;
_root.style.opacity = 0f;
_root.style.backgroundColor = new Color(0.02f, 0.02f, 0.024f, 0.58f);
_root.style.justifyContent = Justify.Center;
_root.style.alignItems = Align.Center;
_root.RegisterCallback<KeyDownEvent>(HandleRootKeyDown);
_root.RegisterCallback<PointerDownEvent>(evt =>
{
if (_closeOnBackdrop && evt.target == _root)
{
BeginClose();
}
evt.StopPropagation();
});
documentRoot.Add(_root);
_stage = new VisualElement();
_stage.pickingMode = PickingMode.Ignore;
_stage.style.position = Position.Relative;
_stage.style.width = Length.Percent(82f);
_stage.style.maxWidth = 900f;
_stage.style.opacity = 0f;
_stage.style.marginTop = 14f;
_root.Add(_stage);
var shadow = new VisualElement();
shadow.pickingMode = PickingMode.Ignore;
shadow.style.position = Position.Absolute;
shadow.style.left = 0f;
shadow.style.right = 0f;
shadow.style.top = 10f;
shadow.style.bottom = -10f;
shadow.style.backgroundColor = new Color(0f, 0f, 0f, 0.09f);
shadow.style.borderTopLeftRadius = 18f;
shadow.style.borderTopRightRadius = 18f;
shadow.style.borderBottomLeftRadius = 18f;
shadow.style.borderBottomRightRadius = 18f;
_stage.Add(shadow);
_panel = new VisualElement
{
pickingMode = PickingMode.Position
};
_panel.style.width = Length.Percent(100f);
_panel.style.minHeight = 360f;
_panel.style.paddingTop = 36f;
_panel.style.paddingRight = 36f;
_panel.style.paddingBottom = 36f;
_panel.style.paddingLeft = 36f;
_panel.style.backgroundColor = Paper;
_panel.style.borderTopWidth = 1f;
_panel.style.borderRightWidth = 1f;
_panel.style.borderBottomWidth = 1f;
_panel.style.borderLeftWidth = 1f;
_panel.style.borderTopColor = Line;
_panel.style.borderRightColor = Line;
_panel.style.borderBottomColor = Line;
_panel.style.borderLeftColor = Line;
_panel.style.borderTopLeftRadius = 18f;
_panel.style.borderTopRightRadius = 18f;
_panel.style.borderBottomLeftRadius = 18f;
_panel.style.borderBottomRightRadius = 18f;
_panel.style.overflow = Overflow.Hidden;
_stage.Add(_panel);
var accent = new VisualElement();
accent.pickingMode = PickingMode.Ignore;
accent.style.position = Position.Absolute;
accent.style.top = 0f;
accent.style.left = 0f;
accent.style.right = 0f;
accent.style.height = 2f;
accent.style.flexDirection = FlexDirection.Row;
_panel.Add(accent);
AddAccentSegment(accent, Blue);
AddAccentSegment(accent, Teal);
AddAccentSegment(accent, Violet);
_eyebrowLabel = new Label("BRISK SERVICE");
_eyebrowLabel.style.color = Muted;
_eyebrowLabel.style.fontSize = 13f;
_eyebrowLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_eyebrowLabel.style.letterSpacing = 1.4f;
_eyebrowLabel.style.height = 18f;
_eyebrowLabel.style.flexShrink = 0f;
_eyebrowLabel.style.marginBottom = 12f;
_panel.Add(_eyebrowLabel);
_titleLabel = new Label();
_titleLabel.style.color = Ink;
_titleLabel.style.fontSize = 30f;
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_titleLabel.style.whiteSpace = WhiteSpace.Normal;
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
_titleLabel.style.minHeight = 40f;
_titleLabel.style.flexShrink = 0f;
_titleLabel.style.marginBottom = 16f;
_panel.Add(_titleLabel);
_messageLabel = new Label();
_messageLabel.style.color = Muted;
_messageLabel.style.fontSize = 21f;
_messageLabel.style.whiteSpace = WhiteSpace.Normal;
_messageLabel.style.unityTextAlign = TextAnchor.UpperLeft;
_messageLabel.style.minHeight = 64f;
_messageLabel.style.flexShrink = 0f;
_messageLabel.style.marginBottom = 22f;
_panel.Add(_messageLabel);
_spacer = new VisualElement();
_spacer.pickingMode = PickingMode.Ignore;
_spacer.style.flexGrow = 1f;
_spacer.style.minHeight = 18f;
_panel.Add(_spacer);
_divider = new VisualElement();
_divider.style.height = 1f;
_divider.style.flexShrink = 0f;
_divider.style.backgroundColor = Line;
_divider.style.marginBottom = 22f;
_panel.Add(_divider);
_confirmButton = new VisualElement
{
focusable = true,
pickingMode = PickingMode.Position
};
_confirmButton.style.height = 58f;
_confirmButton.style.minHeight = 58f;
_confirmButton.style.flexShrink = 0f;
_confirmButton.style.borderTopLeftRadius = 24f;
_confirmButton.style.borderTopRightRadius = 24f;
_confirmButton.style.borderBottomLeftRadius = 24f;
_confirmButton.style.borderBottomRightRadius = 24f;
_confirmButton.style.backgroundColor = Dark;
_confirmButton.style.justifyContent = Justify.Center;
_confirmButton.style.alignItems = Align.Center;
_confirmButton.RegisterCallback<PointerEnterEvent>(_ => _confirmButton.style.backgroundColor = InkSoft);
_confirmButton.RegisterCallback<PointerLeaveEvent>(_ => _confirmButton.style.backgroundColor = Dark);
_confirmButton.RegisterCallback<PointerDownEvent>(_ =>
{
_confirmButton.style.backgroundColor = Ink;
_confirmButton.style.marginTop = 1f;
});
_confirmButton.RegisterCallback<PointerUpEvent>(_ =>
{
_confirmButton.style.backgroundColor = InkSoft;
_confirmButton.style.marginTop = 0f;
});
_confirmButton.RegisterCallback<ClickEvent>(_ => BeginClose());
_confirmButton.RegisterCallback<KeyDownEvent>(HandleButtonKeyDown);
_panel.Add(_confirmButton);
_confirmLabel = new Label();
_confirmLabel.style.color = Color.white;
_confirmLabel.style.fontSize = 18f;
_confirmLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_confirmLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
_confirmLabel.style.height = 58f;
_confirmButton.Add(_confirmLabel);
}
private void Update()
{
if (_visible && _root != null)
{
ApplySafeAreaPadding();
ApplyResponsiveLayout();
}
}
private void BeginClose()
{
if (!_visible || _root == null)
{
return;
}
StartDialogAnimation(PlayCloseAnimation());
}
private void OnGUI()
{
if (!_visible)
{
return;
}
DrawLegacyOverlay();
}
private void DrawLegacyOverlay()
{
EnsureLegacyStyles();
var previousDepth = GUI.depth;
var previousMatrix = GUI.matrix;
var previousColor = GUI.color;
GUI.depth = -10000;
GUI.matrix = Matrix4x4.identity;
DrawLegacyRect(new Rect(0f, 0f, Screen.width, Screen.height), new Color(0.02f, 0.02f, 0.024f, 0.58f));
var scale = GetReadableScale();
ApplyLegacyScale(scale);
var margin = GetDialogMargin(scale);
var padding = GetDialogPadding(scale);
var buttonHeight = GetDialogButtonHeight(scale);
var width = GetDialogWidth(margin);
var contentWidth = width - padding * 2f;
var titleHeight = Mathf.Max(34f, _legacyTitleStyle.CalcHeight(new GUIContent(_titleLabel.text), contentWidth));
var messageHeight = Mathf.Max(48f, _legacyMessageStyle.CalcHeight(new GUIContent(_messageLabel.text), contentWidth));
var minHeight = GetDialogMinHeight(scale, margin);
var contentHeight = padding * 2f + 18f * scale + 12f * scale + titleHeight + 16f * scale + messageHeight + 24f * scale + 1f + 22f * scale + buttonHeight;
var height = Mathf.Clamp(contentHeight, minHeight, Mathf.Max(minHeight, Screen.height - margin * 2f));
var rect = new Rect((Screen.width - width) * 0.5f, (Screen.height - height) * 0.5f, width, height);
DrawLegacyPanel(rect);
var x = rect.x + padding;
var y = rect.y + padding;
GUI.Label(new Rect(x, y, contentWidth, 18f * scale), "BRISK SERVICE", _legacyEyebrowStyle);
y += 30f * scale;
GUI.Label(new Rect(x, y, contentWidth, titleHeight), _titleLabel.text, _legacyTitleStyle);
y += titleHeight + 16f * scale;
GUI.Label(new Rect(x, y, contentWidth, messageHeight), _messageLabel.text, _legacyMessageStyle);
DrawLegacyRect(new Rect(x, rect.yMax - padding - buttonHeight - 22f * scale, contentWidth, 1f), Line);
var buttonRect = new Rect(x, rect.yMax - padding - buttonHeight, contentWidth, buttonHeight);
if (GUI.Button(buttonRect, _confirmLabel.text, _legacyButtonStyle))
{
BeginClose();
}
ConsumeBlockingEvent();
GUI.color = previousColor;
GUI.matrix = previousMatrix;
GUI.depth = previousDepth;
}
private IEnumerator PlayOpenAnimation()
{
_root.style.display = DisplayStyle.Flex;
_root.style.opacity = 0f;
_stage.style.opacity = 0f;
_stage.style.marginTop = 14f;
yield return null;
_root.Focus();
var elapsed = 0f;
while (elapsed < OpenDuration)
{
elapsed += Time.unscaledDeltaTime;
var eased = EaseOutCubic(Mathf.Clamp01(elapsed / OpenDuration));
_root.style.opacity = eased;
_stage.style.opacity = eased;
_stage.style.marginTop = Mathf.Lerp(14f, 0f, eased);
yield return null;
}
_root.style.opacity = 1f;
_stage.style.opacity = 1f;
_stage.style.marginTop = 0f;
}
private IEnumerator PlayCloseAnimation()
{
var callback = _onConfirm;
_onConfirm = null;
var elapsed = 0f;
while (elapsed < CloseDuration)
{
elapsed += Time.unscaledDeltaTime;
var eased = EaseInCubic(Mathf.Clamp01(elapsed / CloseDuration));
var alpha = 1f - eased;
_root.style.opacity = alpha;
_stage.style.opacity = alpha;
_stage.style.marginTop = Mathf.Lerp(0f, 10f, eased);
yield return null;
}
_root.style.display = DisplayStyle.None;
_visible = false;
callback?.Invoke();
}
private void StartDialogAnimation(IEnumerator animation)
{
if (_animation != null)
{
StopCoroutine(_animation);
}
_animation = StartCoroutine(animation);
}
private void ApplySafeAreaPadding()
{
var safeArea = Screen.safeArea;
var basePadding = Mathf.Max(20f, Mathf.Min(Screen.width, Screen.height) * 0.04f);
_root.style.paddingLeft = Mathf.Max(basePadding, safeArea.xMin + basePadding);
_root.style.paddingRight = Mathf.Max(basePadding, Screen.width - safeArea.xMax + basePadding);
_root.style.paddingTop = Mathf.Max(basePadding, Screen.height - safeArea.yMax + basePadding);
_root.style.paddingBottom = Mathf.Max(basePadding, safeArea.yMin + basePadding);
}
private void ApplyResponsiveLayout()
{
if (_panel == null)
{
return;
}
var scale = GetReadableScale();
var margin = GetDialogMargin(scale);
var maxWidth = Mathf.Min(900f, Mathf.Max(320f, Screen.width - margin * 2f));
var minHeight = GetDialogMinHeight(scale, margin);
var padding = GetDialogPadding(scale);
var buttonHeight = GetDialogButtonHeight(scale);
_stage.style.width = Length.Percent(82f);
_stage.style.maxWidth = maxWidth;
_panel.style.minHeight = minHeight;
_panel.style.paddingTop = padding;
_panel.style.paddingRight = padding;
_panel.style.paddingBottom = padding;
_panel.style.paddingLeft = padding;
_panel.style.borderTopLeftRadius = 20f * scale;
_panel.style.borderTopRightRadius = 20f * scale;
_panel.style.borderBottomLeftRadius = 20f * scale;
_panel.style.borderBottomRightRadius = 20f * scale;
_eyebrowLabel.style.fontSize = 12f * scale;
_eyebrowLabel.style.height = 18f * scale;
_eyebrowLabel.style.marginBottom = 12f * scale;
_titleLabel.style.fontSize = 28f * scale;
_titleLabel.style.minHeight = 40f * scale;
_titleLabel.style.marginBottom = 16f * scale;
_messageLabel.style.fontSize = 20f * scale;
_messageLabel.style.minHeight = 64f * scale;
_messageLabel.style.marginBottom = 22f * scale;
_spacer.style.minHeight = 18f * scale;
_divider.style.marginBottom = 22f * scale;
_confirmButton.style.height = buttonHeight;
_confirmButton.style.minHeight = buttonHeight;
_confirmButton.style.borderTopLeftRadius = buttonHeight * 0.5f;
_confirmButton.style.borderTopRightRadius = buttonHeight * 0.5f;
_confirmButton.style.borderBottomLeftRadius = buttonHeight * 0.5f;
_confirmButton.style.borderBottomRightRadius = buttonHeight * 0.5f;
_confirmLabel.style.fontSize = 17f * scale;
_confirmLabel.style.height = buttonHeight;
}
private void HandleRootKeyDown(KeyDownEvent evt)
{
if (_closeOnEscape && evt.keyCode == KeyCode.Escape)
{
BeginClose();
evt.StopPropagation();
return;
}
evt.StopPropagation();
}
private void HandleButtonKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter || evt.keyCode == KeyCode.Space)
{
BeginClose();
evt.StopPropagation();
}
}
private void OnDestroy()
{
if (s_instance == this)
{
s_instance = null;
}
if (_panelSettings != null)
{
Destroy(_panelSettings);
_panelSettings = null;
}
}
private void EnsureLegacyStyles()
{
if (_legacyTitleStyle != null)
{
return;
}
_legacyButtonTexture = CreateTexture(Dark);
_legacyButtonHoverTexture = CreateTexture(InkSoft);
_legacyEyebrowStyle = new GUIStyle
{
fontStyle = FontStyle.Bold,
normal = { textColor = Muted }
};
_legacyTitleStyle = new GUIStyle
{
fontStyle = FontStyle.Bold,
wordWrap = true,
normal = { textColor = Ink }
};
_legacyMessageStyle = new GUIStyle
{
wordWrap = true,
normal = { textColor = Muted }
};
_legacyButtonStyle = new GUIStyle
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
border = new RectOffset(24, 24, 24, 24),
normal =
{
background = _legacyButtonTexture,
textColor = Color.white
},
hover =
{
background = _legacyButtonHoverTexture,
textColor = Color.white
},
active =
{
background = _legacyButtonHoverTexture,
textColor = Color.white
}
};
}
private void ApplyLegacyScale(float scale)
{
_legacyEyebrowStyle.fontSize = Mathf.RoundToInt(12f * scale);
_legacyTitleStyle.fontSize = Mathf.RoundToInt(28f * scale);
_legacyMessageStyle.fontSize = Mathf.RoundToInt(20f * scale);
_legacyButtonStyle.fontSize = Mathf.RoundToInt(17f * scale);
}
private void DrawLegacyPanel(Rect rect)
{
DrawLegacyRect(new Rect(rect.x, rect.y + 12f, rect.width, rect.height), new Color(0f, 0f, 0f, 0.08f));
DrawLegacyRect(rect, Paper);
DrawLegacyRect(new Rect(rect.x, rect.y, rect.width, 1f), Line);
DrawLegacyRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), Line);
DrawLegacyRect(new Rect(rect.x, rect.y, 1f, rect.height), Line);
DrawLegacyRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), Line);
var segment = rect.width / 3f;
DrawLegacyRect(new Rect(rect.x, rect.y, segment, 2f), Blue);
DrawLegacyRect(new Rect(rect.x + segment, rect.y, segment, 2f), Teal);
DrawLegacyRect(new Rect(rect.x + segment * 2f, rect.y, rect.width - segment * 2f, 2f), Violet);
}
private static void ConsumeBlockingEvent()
{
var evt = Event.current;
if (evt == null)
{
return;
}
switch (evt.type)
{
case EventType.MouseDown:
case EventType.MouseUp:
case EventType.MouseDrag:
case EventType.ScrollWheel:
case EventType.KeyDown:
case EventType.KeyUp:
evt.Use();
break;
}
}
private static Texture2D CreateTexture(Color color)
{
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false)
{
hideFlags = HideFlags.HideAndDontSave
};
texture.SetPixel(0, 0, color);
texture.Apply();
return texture;
}
private static void DrawLegacyRect(Rect rect, Color color)
{
var previousColor = GUI.color;
GUI.color = color;
GUI.DrawTexture(rect, Texture2D.whiteTexture);
GUI.color = previousColor;
}
private static void AddAccentSegment(VisualElement parent, Color color)
{
var segment = new VisualElement();
segment.style.flexGrow = 1f;
segment.style.backgroundColor = color;
parent.Add(segment);
}
private static float GetReadableScale()
{
return Mathf.Clamp(Mathf.Min(Screen.width, Screen.height) / 720f, 1f, 1.22f);
}
private static float GetDialogMargin(float scale)
{
return Mathf.Max(24f * scale, Mathf.Min(Screen.width, Screen.height) * 0.06f);
}
private static float GetDialogPadding(float scale)
{
return 34f * scale;
}
private static float GetDialogButtonHeight(float scale)
{
return 56f * scale;
}
private static float GetDialogWidth(float margin)
{
return Mathf.Clamp(Screen.width * 0.82f, 320f, Mathf.Max(320f, Mathf.Min(900f, Screen.width - margin * 2f)));
}
private static float GetDialogMinHeight(float scale, float margin)
{
var availableHeight = Mathf.Max(320f, Screen.height - margin * 2f);
return Mathf.Min(availableHeight, Mathf.Clamp(Screen.height * 0.24f, 360f * scale, 620f));
}
private static float EaseOutCubic(float t)
{
return 1f - Mathf.Pow(1f - t, 3f);
}
private static float EaseInCubic(float t)
{
return t * t * t;
}
private static Color ColorFromHex(int rgb)
{
var r = ((rgb >> 16) & 0xff) / 255f;
var g = ((rgb >> 8) & 0xff) / 255f;
var b = (rgb & 0xff) / 255f;
return new Color(r, g, b, 1f);
}
}

View File

@@ -0,0 +1,98 @@
using System;
using UnityEngine;
public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
{
private static readonly BriskDefaultErrorPresenter InstanceValue = new BriskDefaultErrorPresenter();
private BriskDefaultErrorPresenter()
{
}
public static BriskDefaultErrorPresenter Instance => InstanceValue;
public void ShowBlockingError(BriskBlockingException exception)
{
var message = GetBlockingMessage(exception);
BriskDefaultErrorDialog.Show(
title: GetBlockingTitle(exception),
message: message,
confirmText: "知道了",
onConfirm: () =>
{
var exitHandler = Brisk.IsInitialized ? Brisk.GetRequiredContext().ExitHandler : null;
if (exitHandler != null)
{
exitHandler.Invoke();
return;
}
Application.Quit();
});
Debug.LogError("[Brisk] Blocking error: " + message);
}
public void ShowAuthExpired(BriskAuthExpiredException exception)
{
var message = GetAuthExpiredMessage(exception);
BriskDefaultErrorDialog.Show(
title: "登录已过期",
message: message,
confirmText: "知道了");
Debug.LogWarning("[Brisk] Auth expired: " + (exception != null ? exception.Message : message));
}
private static string GetBlockingTitle(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "服务维护中";
}
if (exception is BriskAccountBannedException)
{
return "账号受限";
}
if (exception is BriskClientUpdateRequiredException)
{
return "需要更新";
}
return "服务暂不可用";
}
private static string GetBlockingMessage(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "服务正在维护,请稍后再试。";
}
if (exception is BriskAccountBannedException)
{
return "当前账号暂时无法使用该服务。";
}
if (exception is BriskClientUpdateRequiredException)
{
return "当前版本过低,请更新后继续。";
}
return "服务暂时不可用,请稍后再试。";
}
private static string GetAuthExpiredMessage(BriskAuthExpiredException exception)
{
if (exception != null && exception.Message != null && exception.Message.IndexOf("missing", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "当前登录状态不可用,请重新登录。";
}
return "登录状态已失效,请重新登录后继续。";
}
}

View File

@@ -249,8 +249,10 @@ internal static class BriskModelMapper
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -276,7 +278,9 @@ internal static class BriskModelMapper
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -309,7 +313,11 @@ internal static class BriskModelMapper
return new BriskSpaceLikeResult
{
Liked = BriskValueReader.GetBool(data, "liked"),
LikeCount = BriskValueReader.GetLong(data, "like_count")
Created = BriskValueReader.GetBool(data, "created"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
};
}

View File

@@ -30,6 +30,10 @@ public sealed class BriskOptions
/// </summary>
public bool ValidateSessionOnInitialize = true;
/// <summary>
/// 初始化恢复的本地会话已过期或失效时,是否使用本地保存的登录身份静默换取新会话。
/// </summary>
public bool AutoReloginOnInitialize = true;
/// <summary>
/// 自定义登录态持久化实现。
/// </summary>
public IBriskTokenStore TokenStore;

Some files were not shown because too many files have changed in this diff Show More