VanguardReMaster/client/Assets/Vanguard/Runtime/UI/LoginView.cs

690 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
}
}