690 lines
24 KiB
C#
690 lines
24 KiB
C#
using System;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityEngine.UI;
|
||
using Vanguard.Api;
|
||
using Vanguard.App;
|
||
using Vanguard.Auth;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>Экран входа по референсу: key art, неоновая панель, меню.</summary>
|
||
public sealed class LoginView
|
||
{
|
||
private const int PanelWidth = 560;
|
||
private const int PanelHeight = 420;
|
||
|
||
private readonly AppConfig _config;
|
||
private readonly AuthService _auth;
|
||
private readonly Action _onAuthenticated;
|
||
|
||
private readonly RectTransform _root;
|
||
private readonly InputField _userName;
|
||
private readonly InputField _password;
|
||
private readonly Image _passwordToggle;
|
||
private readonly Button _loginButton;
|
||
private readonly Text _loginLabel;
|
||
private readonly Button _registerButton;
|
||
private readonly Text _status;
|
||
private readonly StatusTicker _ticker;
|
||
private readonly LoginMenu _menu;
|
||
|
||
private bool _busy;
|
||
private bool _passwordVisible;
|
||
|
||
public LoginView(
|
||
Transform parent,
|
||
AppConfig config,
|
||
AuthService auth,
|
||
Action onAuthenticated)
|
||
{
|
||
_config = config;
|
||
_auth = auth;
|
||
_onAuthenticated = onAuthenticated;
|
||
|
||
_root = UiKit.CreatePanel(parent, "LoginScreen", new Color(0f, 0f, 0f, 0f));
|
||
_root.GetComponent<Image>().raycastTarget = false;
|
||
|
||
LoginBackdrop.Create(_root);
|
||
|
||
var panel = CreatePanel(_root);
|
||
_userName = CreateField(panel, "UserName", "Логин", UserIcon(), 110f);
|
||
_password = CreateField(panel, "Password", "Пароль", LockIcon(), 32f, password: true);
|
||
_passwordToggle = CreatePasswordToggle(panel);
|
||
|
||
var tabs = _root.gameObject.AddComponent<TabFocusDriver>();
|
||
tabs.First = _userName;
|
||
tabs.Second = _password;
|
||
tabs.Submit = OnSubmitShortcut;
|
||
tabs.Cancel = OnCancelShortcut;
|
||
|
||
CreateLink(
|
||
panel,
|
||
"ForgotPassword",
|
||
"Забыли пароль?",
|
||
-14f,
|
||
TextAnchor.MiddleRight,
|
||
new Color(0.37f, 0.69f, 1f),
|
||
OnForgotClicked);
|
||
|
||
_loginButton = CreateLoginButton(panel, out _loginLabel);
|
||
_registerButton = CreateLink(
|
||
panel,
|
||
"Register",
|
||
"Создать аккаунт",
|
||
-128f,
|
||
TextAnchor.MiddleCenter,
|
||
new Color(0.50f, 0.66f, 0.88f),
|
||
OnRegisterClicked);
|
||
|
||
_status = UiKit.CreateText(
|
||
panel,
|
||
"Status",
|
||
string.Empty,
|
||
15,
|
||
new Color(0.62f, 0.72f, 0.88f),
|
||
TextAnchor.UpperCenter);
|
||
_status.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||
_status.verticalOverflow = VerticalWrapMode.Overflow;
|
||
Place(_status.rectTransform, new Vector2(500f, 44f), new Vector2(0f, -172f));
|
||
|
||
CreateTagline(_root);
|
||
CreateCopyright(_root);
|
||
|
||
_ticker = _root.gameObject.AddComponent<StatusTicker>();
|
||
_ticker.Hide = () => SetStatus(string.Empty, StatusColor);
|
||
|
||
_menu = new LoginMenu(_root, _config);
|
||
}
|
||
|
||
/// <summary>Enter отправляет форму; в открытом меню не срабатывает.</summary>
|
||
private void OnSubmitShortcut()
|
||
{
|
||
if (_busy || _menu == null || _menu.IsOpen)
|
||
return;
|
||
|
||
_ = SubmitAsync(register: false);
|
||
}
|
||
|
||
/// <summary>Esc закрывает меню, если оно открыто.</summary>
|
||
private void OnCancelShortcut()
|
||
{
|
||
if (_menu != null && _menu.IsOpen)
|
||
_menu.Close();
|
||
}
|
||
|
||
public void Show(string status = null)
|
||
{
|
||
UiKit.SetActive(_root, true);
|
||
if (!string.IsNullOrEmpty(status))
|
||
SetStatus(status, StatusColor);
|
||
}
|
||
|
||
public void Hide() => UiKit.SetActive(_root, false);
|
||
|
||
// ------------------------------------------------------------ построение
|
||
|
||
private static RectTransform CreatePanel(Transform parent)
|
||
{
|
||
var glow = NeonUi.AddImage(
|
||
parent,
|
||
"PanelGlow",
|
||
NeonUi.Glow(
|
||
PanelWidth + 48,
|
||
PanelHeight + 48,
|
||
24,
|
||
24,
|
||
new Color(0.18f, 0.49f, 1f, 0.24f)),
|
||
new Vector2(PanelWidth + 48, PanelHeight + 48),
|
||
new Vector2(0f, -170f));
|
||
|
||
var panel = UiKit.CreateCenteredCard(
|
||
parent,
|
||
"LoginPanel",
|
||
new Vector2(PanelWidth, PanelHeight),
|
||
Color.white);
|
||
panel.anchoredPosition = new Vector2(0f, -170f);
|
||
panel.GetComponent<Image>().sprite = NeonUi.Body(
|
||
PanelWidth,
|
||
PanelHeight,
|
||
24,
|
||
new Color(0.024f, 0.058f, 0.15f, 0.94f),
|
||
new Color(0.18f, 0.49f, 1f, 0.9f),
|
||
2);
|
||
panel.GetComponent<Image>().raycastTarget = false;
|
||
glow.transform.SetSiblingIndex(panel.GetSiblingIndex() - 1);
|
||
return panel;
|
||
}
|
||
|
||
private static InputField CreateField(
|
||
Transform parent,
|
||
string name,
|
||
string placeholder,
|
||
Sprite icon,
|
||
float y,
|
||
bool password = false)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(InputField));
|
||
var rect = (RectTransform)go.transform;
|
||
rect.SetParent(parent, false);
|
||
Place(rect, new Vector2(480f, 64f), new Vector2(0f, y));
|
||
|
||
var background = go.GetComponent<Image>();
|
||
background.sprite = NeonUi.Body(
|
||
480,
|
||
64,
|
||
10,
|
||
new Color(0.014f, 0.041f, 0.105f, 0.97f),
|
||
new Color(0.22f, 0.53f, 1f, 0.55f),
|
||
1);
|
||
background.type = Image.Type.Simple;
|
||
background.raycastTarget = true;
|
||
|
||
var input = go.GetComponent<InputField>();
|
||
input.targetGraphic = background;
|
||
input.lineType = InputField.LineType.SingleLine;
|
||
input.contentType = password
|
||
? InputField.ContentType.Password
|
||
: InputField.ContentType.Standard;
|
||
input.characterLimit = password ? 128 : 64;
|
||
input.caretColor = new Color(0.55f, 0.82f, 1f);
|
||
input.customCaretColor = true;
|
||
input.selectionColor = new Color(0.22f, 0.53f, 1f, 0.55f);
|
||
input.colors = FieldColors();
|
||
|
||
var text = UiKit.CreateText(rect, "Text", string.Empty, 18, Color.white, TextAnchor.MiddleLeft);
|
||
UiKit.Stretch(text.rectTransform);
|
||
text.rectTransform.offsetMin = new Vector2(52f, 0f);
|
||
text.rectTransform.offsetMax = new Vector2(-44f, 0f);
|
||
text.supportRichText = false;
|
||
UiKit.MakeLiteral(text);
|
||
|
||
var hint = UiKit.CreateText(
|
||
rect,
|
||
"Placeholder",
|
||
placeholder,
|
||
18,
|
||
new Color(0.55f, 0.63f, 0.78f),
|
||
TextAnchor.MiddleLeft);
|
||
UiKit.Stretch(hint.rectTransform);
|
||
hint.rectTransform.offsetMin = new Vector2(52f, 0f);
|
||
hint.rectTransform.offsetMax = new Vector2(-44f, 0f);
|
||
hint.supportRichText = false;
|
||
|
||
input.textComponent = text;
|
||
input.placeholder = hint;
|
||
|
||
NeonUi.AddImage(rect, "Icon", icon, new Vector2(22f, 22f), new Vector2(-212f, 0f));
|
||
return input;
|
||
}
|
||
|
||
private Image CreatePasswordToggle(Transform parent)
|
||
{
|
||
var go = new GameObject(
|
||
"PasswordToggle",
|
||
typeof(RectTransform),
|
||
typeof(Image),
|
||
typeof(Button));
|
||
var rect = (RectTransform)go.transform;
|
||
rect.SetParent(parent, false);
|
||
Place(rect, new Vector2(32f, 32f), new Vector2(208f, 32f));
|
||
|
||
var image = go.GetComponent<Image>();
|
||
image.sprite = EyeIcon();
|
||
image.color = new Color(1f, 1f, 1f, 0.75f);
|
||
image.raycastTarget = true;
|
||
|
||
var button = go.GetComponent<Button>();
|
||
button.targetGraphic = image;
|
||
button.transition = Selectable.Transition.ColorTint;
|
||
button.colors = FieldColors();
|
||
button.onClick.AddListener(TogglePassword);
|
||
|
||
return image;
|
||
}
|
||
|
||
private Button CreateLoginButton(Transform panel, out Text label)
|
||
{
|
||
NeonUi.AddImage(
|
||
panel,
|
||
"LoginButtonGlow",
|
||
NeonUi.Glow(
|
||
508,
|
||
100,
|
||
14,
|
||
12,
|
||
new Color(0.25f, 0.6f, 1f, 0.45f)),
|
||
new Vector2(508f, 100f),
|
||
new Vector2(0f, -70f));
|
||
|
||
var go = new GameObject(
|
||
"LoginButton",
|
||
typeof(RectTransform),
|
||
typeof(Image),
|
||
typeof(Button));
|
||
var rect = (RectTransform)go.transform;
|
||
rect.SetParent(panel, false);
|
||
Place(rect, new Vector2(480f, 72f), new Vector2(0f, -70f));
|
||
|
||
var body = go.GetComponent<Image>();
|
||
body.sprite = NeonUi.Bevel(
|
||
480,
|
||
72,
|
||
14,
|
||
new Color(0.18f, 0.55f, 1f, 1f),
|
||
new Color(0.10f, 0.37f, 0.79f, 1f),
|
||
new Color(0.49f, 0.77f, 1f, 1f),
|
||
2);
|
||
body.type = Image.Type.Simple;
|
||
body.raycastTarget = true;
|
||
|
||
var button = go.GetComponent<Button>();
|
||
button.targetGraphic = body;
|
||
button.transition = Selectable.Transition.ColorTint;
|
||
button.colors = new ColorBlock
|
||
{
|
||
normalColor = Color.white,
|
||
highlightedColor = new Color(1.12f, 1.12f, 1.12f, 1f),
|
||
pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f),
|
||
selectedColor = Color.white,
|
||
disabledColor = new Color(0.55f, 0.55f, 0.55f, 0.6f),
|
||
colorMultiplier = 1f,
|
||
fadeDuration = 0.08f,
|
||
};
|
||
button.onClick.AddListener(OnLoginClicked);
|
||
|
||
label = UiKit.CreateText(rect, "Label", "Войти", 22, Color.white);
|
||
label.fontStyle = FontStyle.Bold;
|
||
UiKit.Stretch(label.rectTransform);
|
||
|
||
return button;
|
||
}
|
||
|
||
private static void CreateTagline(Transform root)
|
||
{
|
||
var emblem = NeonUi.AddAnchored(
|
||
root,
|
||
"TaglineEmblem",
|
||
NeonUi.Body(30, 30, 7, new Color(0.24f, 0.58f, 1f, 0.9f), new Color(0.6f, 0.85f, 1f, 0.9f), 1),
|
||
new Vector2(34f, 34f),
|
||
new Vector2(0f, 0f),
|
||
new Vector2(0.5f, 0.5f),
|
||
new Vector2(58f, 66f));
|
||
emblem.localRotation = Quaternion.Euler(0f, 0f, 45f);
|
||
|
||
var line1 = UiKit.CreateText(
|
||
root,
|
||
"Tagline1",
|
||
"FIGHT TOGETHER.",
|
||
11,
|
||
new Color(0.59f, 0.76f, 1f, 0.68f),
|
||
TextAnchor.MiddleLeft);
|
||
var rect1 = line1.rectTransform;
|
||
rect1.anchorMin = new Vector2(0f, 0f);
|
||
rect1.anchorMax = new Vector2(0f, 0f);
|
||
rect1.pivot = new Vector2(0f, 0.5f);
|
||
rect1.sizeDelta = new Vector2(360f, 16f);
|
||
rect1.anchoredPosition = new Vector2(88f, 74f);
|
||
|
||
var line2 = UiKit.CreateText(
|
||
root,
|
||
"Tagline2",
|
||
"BUILD YOUR FUTURE.",
|
||
11,
|
||
new Color(0.59f, 0.76f, 1f, 0.68f),
|
||
TextAnchor.MiddleLeft);
|
||
var rect2 = line2.rectTransform;
|
||
rect2.anchorMin = new Vector2(0f, 0f);
|
||
rect2.anchorMax = new Vector2(0f, 0f);
|
||
rect2.pivot = new Vector2(0f, 0.5f);
|
||
rect2.sizeDelta = new Vector2(360f, 16f);
|
||
rect2.anchoredPosition = new Vector2(88f, 58f);
|
||
|
||
UiKit.MakeLiteral(line1);
|
||
UiKit.MakeLiteral(line2);
|
||
}
|
||
|
||
private static void CreateCopyright(Transform root)
|
||
{
|
||
var text = UiKit.CreateText(
|
||
root,
|
||
"Copyright",
|
||
"© Cardfight!! Vanguard Fan Project",
|
||
12,
|
||
new Color(0.51f, 0.65f, 0.84f, 0.5f),
|
||
TextAnchor.MiddleRight);
|
||
var rect = text.rectTransform;
|
||
rect.anchorMin = new Vector2(1f, 0f);
|
||
rect.anchorMax = new Vector2(1f, 0f);
|
||
rect.pivot = new Vector2(1f, 0f);
|
||
rect.sizeDelta = new Vector2(420f, 20f);
|
||
rect.anchoredPosition = new Vector2(-28f, 18f);
|
||
UiKit.MakeLiteral(text);
|
||
}
|
||
|
||
// ------------------------------------------------------------ иконки
|
||
|
||
private static Sprite UserIcon()
|
||
{
|
||
var head = NeonUi.Circle(10, new Color(0.62f, 0.8f, 1f, 0.85f));
|
||
var body = NeonUi.Body(
|
||
18,
|
||
10,
|
||
5,
|
||
new Color(0.62f, 0.8f, 1f, 0.85f),
|
||
Color.clear,
|
||
0);
|
||
return Compose(22, 22, new[]
|
||
{
|
||
(head, new Vector2(0f, 5f), new Vector2(10f, 10f)),
|
||
(body, new Vector2(0f, -5f), new Vector2(18f, 10f)),
|
||
});
|
||
}
|
||
|
||
private static Sprite LockIcon()
|
||
{
|
||
var shackle = NeonUi.Ring(14, 2f, new Color(0.62f, 0.8f, 1f, 0.85f));
|
||
var body = NeonUi.Body(
|
||
18,
|
||
14,
|
||
4,
|
||
new Color(0.62f, 0.8f, 1f, 0.85f),
|
||
Color.clear,
|
||
0);
|
||
return Compose(22, 22, new[]
|
||
{
|
||
(shackle, new Vector2(0f, 4f), new Vector2(14f, 14f)),
|
||
(body, new Vector2(0f, -4f), new Vector2(18f, 14f)),
|
||
});
|
||
}
|
||
|
||
private static Sprite EyeIcon()
|
||
{
|
||
var outline = NeonUi.Ring(20, 1.5f, Color.white);
|
||
var pupil = NeonUi.Circle(7, Color.white);
|
||
return Compose(24, 24, new[]
|
||
{
|
||
(outline, Vector2.zero, new Vector2(20f, 20f)),
|
||
(pupil, Vector2.zero, new Vector2(7f, 7f)),
|
||
});
|
||
}
|
||
|
||
/// <summary>Собирает несколько спрайтов в один, чтобы не держать лишние Image.</summary>
|
||
private static Sprite Compose(
|
||
int width,
|
||
int height,
|
||
(Sprite sprite, Vector2 offset, Vector2 size)[] parts)
|
||
{
|
||
var pixels = new Color32[width * height];
|
||
foreach (var part in parts)
|
||
{
|
||
if (part.sprite == null)
|
||
continue;
|
||
|
||
var texture = part.sprite.texture;
|
||
var source = texture.GetPixels32();
|
||
var sourceWidth = texture.width;
|
||
var sourceHeight = texture.height;
|
||
var left = Mathf.RoundToInt(width * 0.5f + part.offset.x - part.size.x * 0.5f);
|
||
var bottom = Mathf.RoundToInt(height * 0.5f + part.offset.y - part.size.y * 0.5f);
|
||
var scaleX = sourceWidth / part.size.x;
|
||
var scaleY = sourceHeight / part.size.y;
|
||
|
||
for (var y = 0; y < part.size.y; y++)
|
||
{
|
||
for (var x = 0; x < part.size.x; x++)
|
||
{
|
||
var targetX = left + x;
|
||
var targetY = bottom + y;
|
||
if (targetX < 0 || targetY < 0 || targetX >= width || targetY >= height)
|
||
continue;
|
||
|
||
var sx = Mathf.Clamp(Mathf.RoundToInt(x * scaleX), 0, sourceWidth - 1);
|
||
var sy = Mathf.Clamp(Mathf.RoundToInt(y * scaleY), 0, sourceHeight - 1);
|
||
var color = source[sy * sourceWidth + sx];
|
||
if (color.a == 0)
|
||
continue;
|
||
|
||
var index = targetY * width + targetX;
|
||
pixels[index] = AlphaBlend(pixels[index], color);
|
||
}
|
||
}
|
||
}
|
||
|
||
var result = new Texture2D(width, height, TextureFormat.RGBA32, false)
|
||
{
|
||
hideFlags = HideFlags.DontSave,
|
||
filterMode = FilterMode.Bilinear,
|
||
wrapMode = TextureWrapMode.Clamp,
|
||
};
|
||
result.SetPixels32(pixels);
|
||
result.Apply(false, false);
|
||
|
||
var sprite = Sprite.Create(
|
||
result,
|
||
new Rect(0f, 0f, width, height),
|
||
new Vector2(0.5f, 0.5f),
|
||
100f);
|
||
sprite.hideFlags = HideFlags.DontSave;
|
||
return sprite;
|
||
}
|
||
|
||
private static Color32 AlphaBlend(Color32 under, Color32 over)
|
||
{
|
||
var alpha = over.a / 255f;
|
||
return new Color32(
|
||
(byte)(over.r * alpha + under.r * (1f - alpha)),
|
||
(byte)(over.g * alpha + under.g * (1f - alpha)),
|
||
(byte)(over.b * alpha + under.b * (1f - alpha)),
|
||
(byte)Mathf.Clamp(over.a + under.a * (1f - alpha), 0f, 255f));
|
||
}
|
||
|
||
// ------------------------------------------------------------ ссылки
|
||
|
||
private static Button CreateLink(
|
||
Transform parent,
|
||
string name,
|
||
string label,
|
||
float y,
|
||
TextAnchor alignment,
|
||
Color color,
|
||
UnityAction onClick)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button));
|
||
var rect = (RectTransform)go.transform;
|
||
rect.SetParent(parent, false);
|
||
Place(rect, new Vector2(480f, 26f), new Vector2(0f, y));
|
||
|
||
var image = go.GetComponent<Image>();
|
||
image.color = new Color(0f, 0f, 0f, 0.001f);
|
||
image.raycastTarget = true;
|
||
|
||
var button = go.GetComponent<Button>();
|
||
button.targetGraphic = image;
|
||
button.transition = Selectable.Transition.None;
|
||
button.onClick.AddListener(onClick);
|
||
|
||
var text = UiKit.CreateText(rect, "Label", label, 15, color, alignment);
|
||
UiKit.Stretch(text.rectTransform);
|
||
|
||
return button;
|
||
}
|
||
|
||
// ------------------------------------------------------------ логика
|
||
|
||
private async void OnLoginClicked()
|
||
{
|
||
await SubmitAsync(register: false);
|
||
}
|
||
|
||
private async void OnRegisterClicked()
|
||
{
|
||
await SubmitAsync(register: true);
|
||
}
|
||
|
||
private void OnForgotClicked()
|
||
{
|
||
SetStatus(
|
||
"Сброс пароля не поддерживается. Обратитесь к администратору.",
|
||
new Color(1f, 0.72f, 0.42f));
|
||
}
|
||
|
||
private void TogglePassword()
|
||
{
|
||
_passwordVisible = !_passwordVisible;
|
||
_password.contentType = _passwordVisible
|
||
? InputField.ContentType.Standard
|
||
: InputField.ContentType.Password;
|
||
_password.ForceLabelUpdate();
|
||
_passwordToggle.color = _passwordVisible
|
||
? new Color(0.45f, 0.78f, 1f, 1f)
|
||
: new Color(1f, 1f, 1f, 0.75f);
|
||
}
|
||
|
||
private async Task SubmitAsync(bool register)
|
||
{
|
||
if (_busy)
|
||
return;
|
||
|
||
var userName = (_userName.text ?? string.Empty).Trim();
|
||
var password = _password.text ?? string.Empty;
|
||
|
||
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
|
||
{
|
||
SetStatus("Введите логин и пароль.", new Color(1f, 0.70f, 0.35f));
|
||
return;
|
||
}
|
||
|
||
SetBusy(true, register ? "Создаём аккаунт…" : "Подключение…");
|
||
|
||
try
|
||
{
|
||
if (register)
|
||
await _auth.RegisterAsync(userName, password);
|
||
else
|
||
await _auth.LoginAsync(userName, password);
|
||
|
||
_password.text = string.Empty;
|
||
SetStatus("Готово.", new Color(0.45f, 0.85f, 0.55f));
|
||
_onAuthenticated();
|
||
}
|
||
catch (ApiException exception)
|
||
{
|
||
SetStatus(FormatError(exception), new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Debug.LogException(exception);
|
||
var detail = exception.Message ?? string.Empty;
|
||
if (detail.IndexOf("Insecure connection", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
SetStatus(
|
||
"Unity блокирует HTTP. Player Settings → Allow downloads over HTTP = Always, затем перезапусти Unity.",
|
||
new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
else
|
||
{
|
||
SetStatus("Не удалось связаться: " + detail, new Color(1f, 0.55f, 0.45f));
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
SetBusy(false, null);
|
||
}
|
||
}
|
||
|
||
private static string FormatError(ApiException exception)
|
||
{
|
||
if (exception.StatusCode == 0)
|
||
return "Нет связи с сервером. Проверьте адрес и запущен ли сервер.";
|
||
|
||
if (exception.Violations.Count > 0)
|
||
{
|
||
var details = string.Join("\n", exception.Violations.Take(3).Select(item => item.Message));
|
||
return exception.Message + "\n" + details;
|
||
}
|
||
|
||
return string.IsNullOrEmpty(exception.Message)
|
||
? "Сервер вернул ошибку " + exception.StatusCode + "."
|
||
: exception.Message;
|
||
}
|
||
|
||
private void SetBusy(bool busy, string status)
|
||
{
|
||
_busy = busy;
|
||
_loginButton.interactable = !busy;
|
||
_registerButton.interactable = !busy;
|
||
_userName.interactable = !busy;
|
||
_password.interactable = !busy;
|
||
|
||
if (_loginLabel != null)
|
||
_loginLabel.text = busy ? "Подключение…" : "Войти";
|
||
|
||
if (status != null)
|
||
SetStatus(status, StatusColor);
|
||
}
|
||
|
||
private void SetStatus(string text, Color color)
|
||
{
|
||
if (_status == null)
|
||
return;
|
||
|
||
_status.text = text ?? string.Empty;
|
||
_status.color = color;
|
||
|
||
if (string.IsNullOrEmpty(text))
|
||
{
|
||
_ticker.HideAt = 0f;
|
||
return;
|
||
}
|
||
|
||
_ticker.HideAt = Time.unscaledTime + 6f;
|
||
}
|
||
|
||
// ------------------------------------------------------------ утилиты
|
||
|
||
private static readonly Color StatusColor = new Color(0.66f, 0.74f, 0.90f);
|
||
|
||
private static void Place(RectTransform rect, Vector2 size, Vector2 position)
|
||
{
|
||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||
rect.sizeDelta = size;
|
||
rect.anchoredPosition = position;
|
||
}
|
||
|
||
private static ColorBlock FieldColors()
|
||
{
|
||
return new ColorBlock
|
||
{
|
||
normalColor = Color.white,
|
||
highlightedColor = new Color(1.15f, 1.15f, 1.15f, 1f),
|
||
pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f),
|
||
selectedColor = new Color(1.15f, 1.15f, 1.15f, 1f),
|
||
disabledColor = new Color(0.6f, 0.6f, 0.6f, 0.5f),
|
||
colorMultiplier = 1f,
|
||
fadeDuration = 0.08f,
|
||
};
|
||
}
|
||
|
||
/// <summary>Скрывает статус через несколько секунд.</summary>
|
||
private sealed class StatusTicker : MonoBehaviour
|
||
{
|
||
public float HideAt;
|
||
public Action Hide;
|
||
|
||
private void Update()
|
||
{
|
||
if (HideAt <= 0f || Time.unscaledTime < HideAt)
|
||
return;
|
||
|
||
HideAt = 0f;
|
||
Hide?.Invoke();
|
||
}
|
||
}
|
||
}
|
||
}
|